返回文章列表

KarateAPI伺服器模擬與測試最佳化

本文介紹如何使用 Karate 建立模擬 API 伺服器,並示範如何結合場景大綱、外部資料檔案和程式化資料生成技術來最佳化測試案例,提升測試效率並減少程式碼重複。涵蓋了定義模擬 API 端點、在測試中啟動模擬伺服器、使用場景大綱進行資料驅動測試、從外部 JSON 檔案讀取測試資料,以及使用 JavaScript

Web 開發 測試

Karate 框架除了 API 測試外,也支援模擬 API 伺服器功能,讓開發者在沒有實際後端服務的情況下進行前端或客戶端測試。透過定義 mocks.feature 檔案,可以模擬各種 API 端點和回應,例如根據 ID 查詢特定資料或處理未定義請求。在測試程式碼中,使用 karate.start 方法啟動模擬伺服器,並透過 callonce 關鍵字確保只啟動一次。為了簡化測試程式碼,Karate 提供了呼叫外部 Feature 檔案和傳遞 JSON 引數的功能,提升程式碼的可維護性。此外,Karate 也支援資料驅動測試,可使用場景大綱搭配範例表格或外部 JSON 檔案,有效減少程式碼重複並提升測試覆寫率。更進一步,可以利用 JavaScript 函式生成更複雜的測試資料,或使用設定場景和 karate-config.js 檔案預先設定測試資料,讓測試流程更具彈性。

使用 Karate 自定義與最佳化測試 - 模擬 API 伺服器

Karate 不僅可用於測試,也可以模擬 API 伺服器以進行測試。這種模擬方式可以讓開發者在不依賴實際後端服務的情況下進行前端或客戶端測試。

定義模擬 API

首先,我們在 mocks.feature 檔案中定義模擬 API。我們從定義一個包含魔術師(magicians)資料的 JSON 結構開始:

[
  { id: 1, name: "Penn" },
  { id: 2, name: "Teller" }
]

背景定義

Background 中,我們定義了一個 JSON 結構來表示魔術師資料。

Background:
* json magicians = 
"""
[
  { id: 1, name: "Penn" },
  { id: 2, name: "Teller" }
]
"""

定義 /magicians 端點

接下來,我們定義了 /magicians 端點。當接收到 GET 請求時,它將傳回完整的 magicians JSON 物件:

Scenario: pathMatches('/magicians') && methodIs('get')
* def response = magicians

這裡,pathMatches('/magicians') 確保只有當請求的路徑是 /magicians 時才會觸發該情境,而 methodIs('get') 則進一步限制了只有 GET 請求才會觸發。

定義 /magician/{id} 端點

我們還定義了一個更複雜的 /magician/{id} 端點,用於根據 ID 傳回特定的魔術師:

Scenario: pathMatches('/magician/{id}') && methodIs('get')
* def index = parseInt(pathParams.id) - 1
* def isValid = index > -1 && index < magicians.length
* def responseDelay = 3000
* def response = isValid ? magicians[index] : ''
* def responseStatus = isValid ? 200 : 204

萬能捕捉情境

最後,我們定義了一個萬能捕捉情境,用於處理其他未被明確定義的請求:

Scenario:
* def responseStatus = 500

從測試中啟動模擬伺服器

為了測試我們的模擬 API,我們建立了一個新的 feature 檔案,並在其中啟動模擬伺服器:

Feature: Testing a mock server
Background:
* def start = () => karate.start('mocks.feature').port
* def port = callonce start
* url 'http://localhost:' + port

Scenario: Get one magician
When path 'magician/2'
And method get
* match response.name == 'Teller'

內容解密:

  1. def start = () => karate.start('mocks.feature').port:這行程式碼定義了一個箭頭函式 start,該函式使用 karate.start() 方法啟動 mocks.feature 檔案中定義的模擬伺服器,並傳回該伺服器的埠號。

  2. def port = callonce start:這裡使用 callonce 關鍵字確保 start 方法只被呼叫一次,即使有多個測試場景。這樣可以防止為每個測試場景都啟動一個新的模擬伺服器。

  3. * url 'http://localhost:' + port:將測試的基本 URL 設定為模擬伺服器的地址和埠。

  4. When path 'magician/2'And method get:這兩行程式碼構建了一個 GET 請求到 /magician/2 端點。

  5. * match response.name == 'Teller':驗證回應中的 name 欄位是否為 'Teller',從而檢查我們的模擬 API 是否按預期工作。

使用Karate作為模擬伺服器

Karate不僅可以用於測試API,還可以用作模擬伺服器(mock server),以模擬API的回應。這對於測試客戶端程式碼或在開發過程中模擬外部服務非常有用。

在測試程式碼中啟動模擬伺服器

在測試程式碼中,我們可以啟動一個模擬伺服器來模擬特定的API回應。例如:

* def server = karate.start({ port: 0, mock: 'mocks.feature' })
* def port = server.port
* url 'http://localhost:' + port + '/magician/2'
* method get
* match response == { id: 2, name: 'Teller' }

在這個例子中,我們使用karate.start方法啟動了一個模擬伺服器,並指定了mocks.feature檔案作為模擬定義。然後,我們發送了一個GET請求到/magician/2,並驗證了回應是否符合預期。

內容解密:

  1. karate.start 方法用於啟動模擬伺服器。
  2. port: 0 表示讓系統自動選擇一個可用的埠。
  3. mock: 'mocks.feature' 指定了包含模擬定義的Feature檔案。
  4. server.port 取得了模擬伺服器實際使用的埠號。

測試模擬伺服器的catch-all場景

我們也可以測試模擬伺服器的catch-all場景,即當請求的路徑沒有在模擬定義中找到匹配時,伺服器應該傳回500狀態碼。

Scenario: 不存在的端點
When path 'rabbits'
And method get
Then match responseStatus == 500

內容解密:

  1. path 'rabbits' 指定了請求的路徑。
  2. method get 指定了請求的方法為GET。
  3. match responseStatus == 500 驗證了回應狀態碼是否為500。

使用獨立的模擬伺服器

除了在測試程式碼中啟動模擬伺服器外,Karate還支援以獨立模式執行模擬伺服器。這可以透過使用Karate的standalone模式實作。

java -jar karate.jar -m src/test/java/examples/mockserver/mocks.feature

這樣,模擬伺服器就會在8080埠上啟動,並可以使用http://localhost:8080作為基礎URL進行測試。

內容解密:

  1. java -jar karate.jar 命令用於執行Karate的standalone模式。
  2. -m 引數指定了包含模擬定義的Feature檔案。
  3. 模擬伺服器預設使用8080埠。

使測試更簡潔

為了使測試更簡潔和可維護,Karate提供了一些功能,如呼叫其他Feature檔案和使用JSON引數。

呼叫其他Feature檔案

我們可以將通用的功能提取到單獨的Feature檔案中,並在其他測試中呼叫它們。例如:

@ignore @report=false
Feature: Say hello feature
Scenario: Greet a person
* print "Hello", name

然後,在其他測試中呼叫這個Feature:

Scenario: 自動存取名稱
* def name = "Benjamin"
* call read('sayhello.feature')

內容解密:

  1. @ignore@report=false 標籤用於忽略這個Feature並隱藏其步驟在測試報告中。
  2. call read('sayhello.feature') 用於呼叫另一個Feature檔案。
  3. name 變數在呼叫前定義,並在被呼叫的Feature中使用。

傳遞JSON引數

我們也可以將JSON引數傳遞給被呼叫的Feature:

Scenario: 以引數形式傳遞名稱
* def params = {name: "Anne"}
* call read('sayhello.feature') params

內容解密:

  1. params 物件包含了要傳遞給被呼叫Feature的引數。
  2. 在被呼叫的Feature中,可以直接使用這些引數。

自訂與最佳化Karate測試

使用資料驅動的場景大綱最佳化測試

在進行測試時,若多個場景具有相似的步驟,很容易造成程式碼重複的問題。當需要對這些步驟進行修改時,就必須在多個場景中個別進行調整,這種情況被稱為「霰彈槍手術」(shotgun surgery),因為相同的微小變更需要在多個地方重複進行。

資料驅動測試的優勢

資料驅動測試是一種軟體測試方法,透過從資料中衍生測試案例,能夠建立多個包含不同輸入資料和預期輸出的測試案例。Karate 提供了多種方法來加速資料驅動測試的建立過程,透過重複使用相同的測試程式碼並提供額外的資料來源,將其轉換為更詳細的測試案例。

避免使用背景場景重複程式碼

我們可以將共同的步驟定義為 Background,在同一個功能檔案中的任何場景執行之前都會先執行這些步驟。之前在某些範例中,我們曾經將共同的步驟移到 Background 中,例如設定基礎 URL 和進行初始請求。

範例:使用場景大綱

Scenario Outline: 一般的大綱
    * print '<name>'
Examples:
    | name   |
    | Eamon  |
    | Ginny  |

在此範例中,我們定義了一個包含不同值的範例表格,每一行代表不同的資料。第一行包含欄位的標題,這裡是 name。對於每一行,場景都會獨立執行。因此,Karate 會將這個場景轉換為兩個獨立的場景,分別對應 EamonGinny。輸出結果如下:

21:55:11.549 [pool-1-thread-1] INFO com.intuit.karate - [print] Eamon
21:55:11.549 [pool-1-thread-2] INFO com.intuit.karate - [print] Ginny

在範例表格中使用 JSON

Karate 允許在範例表格中直接使用 JSON 資料,而不需要像 Cucumber 那樣需要自訂表格轉換器。

Scenario Outline: 在資料表格中使用 JSON
    * def color = traits.color
    * print '<name> is', color
Examples:
    | name   | traits!              |
    | Eamon  | {color: 'red'}       |
    | Ginny  | {color: 'black'}     |

在這個範例中,我們在 traits 欄位中使用了 JSON 結構,並透過在欄位名稱後加上 ! 符號,告訴 Karate 將該欄位的值轉換為適當的資料型別。否則,這些值將被視為字串,存取 traits.color 時會出現錯誤。

執行這個場景後,輸出結果如下:

22:17:41.548 [pool-1-thread-1] INFO com.intuit.karate - [print] Eamon is red
22:17:41.548 [pool-1-thread-2] INFO com.intuit.karate - [print] Ginny is black

使用外部檔案作為資料來源

當範例表格變得龐大時,使用資料檔案會是一個更好的選擇。Karate 可以讀取 JSON 檔案並將其轉換為可用的變數。

假設我們有一個名為 animals.json 的檔案,內容如下:

[
  {"name": "Ginny", "color": "black"},
  {"name": "Eamon", "color": "red"}
]

我們可以在範例表格中直接使用這個檔案:

Scenario Outline: 使用檔案中的資料
    * print name, 'is', color
Examples:
    | read('animals.json') |

透過使用 read 函式讀取檔案,Karate 會將 JSON 結構轉換為可以直接在測試場景中使用的變數。因此,我們可以直接存取 namecolor 這兩個欄位。

輸出結果與之前相同:

22:04:23.388 [pool-1-thread-1] INFO com.intuit.karate - [print] Eamon is red
22:04:23.389 [pool-1-thread-2] INFO com.intuit.karate - [print] Ginny is black

這種方法使得測試資料的管理更加靈活和方便,並且能夠支援更複雜的測試案例。

內容解密:

  1. 資料驅動測試:利用外部資料來源驅動測試案例的執行,使得測試更具彈性和可擴充套件性。
  2. Background 的使用:將共同的步驟抽取到 Background 中,避免重複程式碼。
  3. Scenario Outline 的應用:透過範例表格簡化相似場景的撰寫,利用 <變數> 來代表不同的輸入值。
  4. JSON 在範例表格中的應用:直接在範例表格中使用 JSON 資料,並透過 ! 符號指定資料型別。
  5. 使用外部 JSON 檔案:將測試資料儲存在外部 JSON 檔案中,並透過 read 函式讀取,使得測試資料的管理更加靈活。

自訂與最佳化Karate測試

從功能檔生成測試資料

另一種指定測試資料的方法是使用特定的功能來為您產生測試資料。在這個例子中,我們結合呼叫外部功能和資料表格的使用。這意味著我們不會從場景大綱中建立多個場景,而是使用更複雜的資料建立一個單獨的場景。

animal-title.feature

@ignore @report=false
Feature: 產生資料
Scenario: 取得動物
* def title = name + ' the ' + animal

這個輔助功能不應該被獨立呼叫,因此像之前一樣,我們使用@ignore@report=false標記它。

它的目的是根據名稱和動物型別產生完整的標題;例如,如果我們傳遞animalcatnameGinny,它應該產生標題Ginny the cat。您可以看到這個功能只是假設nameanimal變數存在。同時,它設定了一個新的變數title,我們希望在呼叫場景中使用它。

呼叫場景範例

Scenario: 功能變數
* table animals
    | animal | name |
    | 'cat'   | 'Ginny' |
    | 'dog'   | 'Eamon' |
* def animalsWithTitles = call read('animal-title.feature') animals
* print animalsWithTitles
* def onlyTitles = $animalsWithTitles[*].title
* print onlyTitles

步驟解析

  1. 建立一個包含animalname欄位的資料表格,並將其儲存在animals變數中。Karate會自動將其轉換為JSON格式。
  2. 讀取並呼叫animal-title.feature檔案,將animals變數作為引數傳遞。然後,animalname變數在功能內自動可用。
  3. animal-title.feature內部,根據animalname建立title變數。這個title變數會自動新增到傳遞的JSON結構中。由於使用了call read,被呼叫功能的完整傳回值可以被指定給新的變數animalsWithTitles

輸出結果

[
  {
    "name": "Ginny",
    "title": "Ginny the cat",
    "animal": "cat"
  },
  {
    "name": "Eamon",
    "title": "Eamon the dog",
    "animal": "dog"
  }
]
  1. 使用JSONPath從animalsWithTitles變數中提取標題到陣列中:
def onlyTitles = $animalsWithTitles[*].title
  1. 最終輸出動物的標題:
[
  "Ginny the cat",
  "Eamon the dog"
]

程式碼詳細解析:

// 建立包含動物名稱的資料表格
* table animals
    | animal | name |
    | 'cat'   | 'Ginny' |
    | 'dog'   | 'Eamon' |

內容解密:

  • table animals建立了一個名為animals的變數,並將資料表格轉換為JSON陣列。
  • 資料表格的每一列代表一個JSON物件,例如第一行資料轉換為 {"animal": "cat", "name": "Ginny"}
// 呼叫外部功能並傳遞引數
* def animalsWithTitles = call read('animal-title.feature') animals

內容解密:

  • call read('animal-title.feature') animals讀取並執行外部功能檔案,並將 animals JSON陣列作為引數傳入。
  • Karate會自動遍歷 animals陣列,並對每個元素執行 animal-title.feature中的邏輯。
  • 傳回結果是一個包含所有執行結果的JSON陣列。

使用設定場景

從Karate 1.3.0版本開始,可以使用所謂的設定場景(setup scenarios)。這些是特殊的帶有標籤的場景,包含可以被其他場景呼叫和使用的設定或資料建立程式碼:

設定場景範例

@setup
Scenario:
* def animals = read('animals.json')

其他場景現在可以使用這個設定,例如,在表格中這樣使用:

Scenario Outline: 來自檔案的資料
* print name, 'is', color
Examples:
| karate.setup().animals |

使用特殊的 karate.setup() 方法,我們可以存取所有來自 @setup 場景的變數。在這種情況下,我們使用設定場景中的 animals 變數JSON來填充我們的範例表格。

在karate-config.js中設定資料

您也可以直接從 karate-config.js 呼叫功能檔。這使您能夠在任何測試執行之前設定資料。

karate-config.js 範例

function fn() {
  var tweety = { name: "Tweety", animal: "bird" }
  var config = {
    bird: karate.call('animal-title.feature', tweety)
  }
  return config;
}

這裡初始化了 tweety 變數,其格式符合功能檔的預期。使用 karate.call,我們可以呼叫它。就像之前在場景內部使用的呼叫操作一樣,這也接受可選引數以傳遞給功能檔。其餘部分如我們已經知道的那樣:標題 Tweety the bird 被新增到傳遞的JSON中,然後作為全域 bird 變數可用,因此場景可以使用它:

使用範例

Scenario: 從karate-config.js讀取
* print bird.title

輸出結果:

12:11:13.925 [pool-1-thread-1] INFO com.intuit.karate -
[print] Tweety the bird