12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

KarateによるAPIのシナリオテスト自動化 #07 Test-Double

Posted at

はじめに

前回(#06 Assertions-Contains)は、Karate における Assert に関して、Containsの使い方についてて確認しました。

今回は、テストダブルといって、これまでのAPIを呼び出す側(ここでは「テストクライアント」と呼びます)ではなく、テスト対象から呼び出される側(ここでは「サーバーモック」と呼びます。「テストダブル」と同様の意味として利用します)を、Karateで実現する方法を確認します。

RESTでのテストクライアントのツールは、色々とありますが、Karateでは、このテストダブルの機能があると分かって、私としては、応用の範囲がかなり広がりました。
これができると、以下のように、テスト対象が別のサービスに依存している場合のテストが容易になります。

image.png

マイクロサービス化されたシステムなど、テスト対象が、別のサービスを呼び出すようなことは、非常に多くなってきました。
これまでは、独自のモックを作成していたりしたのですが、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に合わせて、モックシナリオを以下のように作成しました。

rentacycles-service-mock.feature
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)でテストしていることになります。

image.png

モックシナリオの解説

今回の内容は、以下に登録しています。

その上で、いくつか、モックシナリオのポイントを解説します。

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 に対する処理

requestrequestParams を利用して、リクエストのデータを取得することができます。

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)の方に一致するとして動作してしまいます。
        1. Scenario: methodIs('get') && pathMatches('/rentacycles')
        1. Scenario: methodIs('get') && pathMatches('/rentacycles') && paramValue('available') == 'true'
  • 値の設定などは、処理の内容によって工夫が必要。

    • 例えば、setを利用したい場合でも、コマンドの呼び出し時と、if指定時の関数呼び出し時では、記述の仕方が異なります。この辺りは、Karateの関数などを把握しておく必要があります。
      • コマンドの呼び出し: * set param = 100
      • 関数の呼び出し: * eval if (data == true) karate.set('param', 100)
  • 関数の利用のしどころを考える。

まとめ

サーバーモックを作成するのに、WireMockでは約350ライン程度の実装が必要だったモノが、今回、Karateで同等の内容を約60ライン程度で実現ができました。
ステートフルなサーバーモックを、DSLで実現できるのは、とても便利だと思います。
最近では、この機能を利用して、フロントエンドを開発するときのバックエンドのAPIをモック化して開発する、といったことなどもしています。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?