はじめに
前回(#06 Assertions-Contains)は、Karate における Assert に関して、Containsの使い方についてて確認しました。
今回は、テストダブルといって、これまでのAPIを呼び出す側(ここでは「テストクライアント」と呼びます)ではなく、テスト対象から呼び出される側(ここでは「サーバーモック」と呼びます。「テストダブル」と同様の意味として利用します)を、Karateで実現する方法を確認します。
RESTでのテストクライアントのツールは、色々とありますが、Karateでは、このテストダブルの機能があると分かって、私としては、応用の範囲がかなり広がりました。
これができると、以下のように、テスト対象が別のサービスに依存している場合のテストが容易になります。
マイクロサービス化されたシステムなど、テスト対象が、別のサービスを呼び出すようなことは、非常に多くなってきました。
これまでは、独自のモックを作成していたりしたのですが、Karateを使えば、独立性の高いサービス単体でのテストが可能になります。
サーバーモック(テストダブル)としての Karate シナリオ
サーバーモックとしてKarateを動作させる場合は、これまでと同様に Feature ファイルを作成しますが、シナリオの書き方が変わります。
Scenario: pathMatches('/cats') && methodIs('post')
* def cat = request
* def id = uuid()
* set cat.id = id
* eval cats[id] = cat
* def response = cat
Scenario: pathMatches('/cats')
* def response = $cats.*
上記のように、Scenario
の部分に、呼び出されるURIの定義を指定します。
request/response などは予約語になっており、それぞれ受信データの内容を取得したり、応答データの内容を指定したりすることが可能になっています。
詳細は、以下のあたりを参照。
テストシナリオ
ここでは、以前の「#03 Writing Scenarios」で作成したサーバーモックを、Karateで実現してみます。
「#03 Writing Scenarios」の際は、WireMock を利用して実現していました。
ただ、一点課題があって、WireMockの場合、固定値のレスポンスを返すようなケースは簡単なのですが、同じAPIでも状態によって戻り値が変わるようなケースは、カスタマイズの実装が必要であり、ちょっと大変でした。
「#03 Writing Scenarios」では、「空き車両一覧取得」のAPIが、これに該当するもので、APIとしては同じリクエストになるのですが、戻り値は状態によって変わるのを実現することが必要でした。
今回は、これをKarateで実現してみます。
モックシナリオ作成
呼び出されるAPIに合わせて、モックシナリオを以下のように作成しました。
Feature: レンタサイクルAPIのサービスモック
Background:
* configure cors = true
* def rentacycles =
"""
[
{id: 'A001', rent: false},
{id: 'A002', rent: false},
{id: 'A003', rent: false},
{id: 'A004', rent: true},
{id: 'A005', rent: true}
]
"""
# -----------------------------------------------
# 空き車両一覧取得
# GET:/rentacycles?available=true
# -----------------------------------------------
Scenario: methodIs('get') && pathMatches('/rentacycles') && paramValue('available') == 'true'
* def availables = karate.jsonPath(rentacycles, "$[?(@.rent==false)]")
* def response = availables
# -----------------------------------------------
# 車両一覧取得
# GET:/rentacycles
# -----------------------------------------------
Scenario: methodIs('get') && pathMatches('/rentacycles')
* def response = rentacycles
# -----------------------------------------------
# レンタル処理
# POST:/rentacycles/rent
# -----------------------------------------------
Scenario: methodIs('post') && pathMatches('/rentacycles/rent')
* def requestBody = request
* def id = requestBody.id
* def target = karate.jsonPath(rentacycles, "$[?(@.id=='" + id + "')]")[0]
* print 'rental target : ', target
* eval if (target == null) karate.abort()
# 既にレンタル済みの場合は、409をリターンして終了
* eval if (target.rent == true) karate.set('responseStatus', 409)
* eval if (responseStatus == 409) karate.abort()
# レンタル可能な場合は、状態を変更して200をリターン
* set target.rent = true
* def response = {result : true}
# -----------------------------------------------
# 返却処理
# POST:/rentacycles/return
# -----------------------------------------------
Scenario: methodIs('post') && pathMatches('/rentacycles/return')
* def requestBody = request
* def id = requestBody.id
* def target = karate.jsonPath(rentacycles, "$[?(@.id=='" + id + "')]")[0]
* print 'rental target : ', target
* eval if (target == null) karate.abort()
# 状態を変更して200をリターン
* set target.rent = false
* def response = {result : true}
# -----------------------------------------------
# API が一致しない場合
# -----------------------------------------------
Scenario:
* def responseStatus = 404
サーバーモックの起動
サーバーモックを起動する場合は、Standalone JAR を利用すると便利です。
これまでは、Karate のシナリオを Maven か Gradle で実行してきていますが、実は、Karateのjarを利用することで、単体として実行することも可能です。
まず、以下から Standalone JAR を取得します。
このjarを使って、以下のように Feature ファイルを実行します。
$ java -jar karate.jar -p 8089 -m rentacycles-service-mock.feature
オプション | 説明 |
---|---|
-p | サーバーモックを起動するポート番号を指定します。 |
-m | 指定したシナリオを、サーバーモックとして起動します。 |
指定した Feature ファイルを正常に読み込めたら、以下のようなかたちで起動状態になります。
Feature ファイルの内容に誤りがあると、エラーとなって起動されません。
10:03:31.986 [main] INFO com.intuit.karate.netty.Main - Karate version: 0.9.2
10:03:32.742 [main] INFO com.intuit.karate - backend initialized
10:03:32.993 [main] INFO c.intuit.karate.netty.FeatureServer - server started - http://127.0.0.1:8089
テストの実行
モックサーバーが起動できたので、その状態で、これまで通り、テストシナリオを実行します。
$ mvn clean test -Dtest=RentaCyclesRunner
実行結果は、以下の通り、
WireMockで実現した内容を、Karateで置き換えてみましたが、元の通り、正常に実行が完了しました!
---------------------------------------------------------
feature: classpath:examples/rentacycles/rentacycles.feature
scenarios: 2 | passed: 2 | failed: 0 | time: 1.0908
---------------------------------------------------------
HTML report: (paste into browser to view) | Karate version: 0.9.2
file:/MyWork/example-karate/target/surefire-reports/examples.rentacycles.rentacycles.html
---------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.205 sec
Results :
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
この内容は、以下のような構成(Karate vs Karate)でテストしていることになります。
モックシナリオの解説
今回の内容は、以下に登録しています。
-
https://github.com/takanorig/example-karate
- /src/mock/java/examples/rentacycles/rentacycles-service-mock.feature
- /src/test/java/examples/rentacycles/rentacycles.feature
その上で、いくつか、モックシナリオのポイントを解説します。
Background
Background
は、モックシナリオ全体における初期化処理です。
ここで変数などを指定しておくと、各APIが呼び出された際に、API横断でその変数を利用できます。
また、以下のように configure cors
を指定しておくと、CORS対応がされます。
Background:
* configure cors = true
この場合、レスポンスヘッダに、自動的に以下の内容が指定されます。
Allow: GET, HEAD, POST, PUT, DELETE, PATCH
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, PATCH
Scenario
GET:/rentacycles?available=true
のようなAPIに対するモックを作成する場合、以下のように記述します。
Scenario: methodIs('get') && pathMatches('/rentacycles') && paramValue('available') == 'true'
関数 | 説明 |
---|---|
methodIs | HTTPメソッドの種類を指定します。 |
pathMatches | モック化するAPIのURIパターンを指定します。 |
paramValue | クエリストリングのパラメータがある場合に、そのパラメータの内容を指定します。 |
request に対する処理
request
や requestParams
を利用して、リクエストのデータを取得することができます。
Scenario: pathMatches('/cats') && methodIs('post')
* def cat = request
response に対する処理
response
に指定したデータがレスポンスのデータとなります。
また、 responseStatus
に指定した値がレスポンスのHTTPステータスとなります。
Scenario: pathMatches('/v1/cats/{id}') && methodIs('get')
* def response = cats[pathParams.id]
* def responseStatus = response ? 200 : 404
注意点
実際にモックシナリオを作成してみて、以下のような点に注意する必要があると感じました。
-
APIの一致判定は、シナリオで定義された上から順番に行われる。
- 今回の場合、以下の2つは、パラメータの有無の違いになりますが、1)の方を先に定義されると、2)よりも前に1)の方に一致するとして動作してしまいます。
-
Scenario: methodIs('get') && pathMatches('/rentacycles')
-
Scenario: methodIs('get') && pathMatches('/rentacycles') && paramValue('available') == 'true'
-
- 今回の場合、以下の2つは、パラメータの有無の違いになりますが、1)の方を先に定義されると、2)よりも前に1)の方に一致するとして動作してしまいます。
-
値の設定などは、処理の内容によって工夫が必要。
- 例えば、setを利用したい場合でも、コマンドの呼び出し時と、if指定時の関数呼び出し時では、記述の仕方が異なります。この辺りは、Karateの関数などを把握しておく必要があります。
- コマンドの呼び出し:
* set param = 100
- 関数の呼び出し:
* eval if (data == true) karate.set('param', 100)
- コマンドの呼び出し:
- 例えば、setを利用したい場合でも、コマンドの呼び出し時と、if指定時の関数呼び出し時では、記述の仕方が異なります。この辺りは、Karateの関数などを把握しておく必要があります。
-
関数の利用のしどころを考える。
- 今回は、関数を定義せず、すべてKarateのDSL定義で実現しましたが、もう少し複雑な処理が必要になる場合は、関数を定義して利用する方が簡単だと思います。
- モックシナリオの場合でも、Karateの関数は利用できるので、それらの活用も考えると良いでしょう。
まとめ
サーバーモックを作成するのに、WireMockでは約350ライン程度の実装が必要だったモノが、今回、Karateで同等の内容を約60ライン程度で実現ができました。
ステートフルなサーバーモックを、DSLで実現できるのは、とても便利だと思います。
最近では、この機能を利用して、フロントエンドを開発するときのバックエンドのAPIをモック化して開発する、といったことなどもしています。