見出し画像

APIテスト自動化ツール「Karate」現場ですぐに使える実装パターン8選(前編)

はじめに

こんにちは、QAメンバとしてテスト自動化を行っている柏木です。

以前、弊社メンバーより『APIテストの自動化は行っていますか?』というテーマでブログ掲載しAPIテストの重要性やAPIテストツールである「Karate」の特徴やメリットについてご紹介しました。
APIテスト自動化のススメ ~テストの悩みをKarateで解決~|SHIFT Group 技術ブログ|note

今回は、そのブログ内で紹介しました「Karate」の実践編として、
より具体的な実装方法についてお話したいと思っておりますが、
基本的内容は、公式ドキュメントやブログ記事など参考にできる情報が巷では多いため私が実業務の中で良く使用する実装パターン(8選)に絞ってご紹介したいと思います。

実装パターン紹介にあたりサンプルプロジェクトを用意しました。事前準備を行った後、パターン紹介と合わせて実際に実行していこうと思います。

※サンプルプロジェクトはGitHubのリポジトリから取得ください。

事前準備

各設定ファイルの内容確認と、テスト実行用のモックサーバーを用意していきます。
※本稿では、ビルドツールはGradle。Karate実行用のJavaはAdoptOpenJDK11を使用していきます。

Exampleのファイル構成

まずは、サンプル(Example)プロジェクト構成です。

karateExample
│─ build.gradle
│─ gradlew
│─ gradlew.bat
│─ README.md
│─ settings.gradle
└─ src
    └─ test
        └─ java
            │─ karate-config.js
            ├─ mock
            │  │─ karate.jar
            │  │─ mock.feature
            │  │─ SequenceUtils.java
            │  │─ server.bat
            └─ scenarios
                │─ sample.feature
                │─ ScenarioRunner.java
                │─ strUtils.java
                │─ userInfoRegister.feature
                └─ data
                    └─ testData.json

build.gradle

Karateバージョンは'1.1.0'で用意します。

// build.gradle
plugins {
    id 'java'
}

ext {
    karateVersion = '1.1.0'
}

repositories {
    mavenCentral()
}

dependencies {
    testCompile "com.intuit.karate:karate-junit5:${karateVersion}"
    implementation "net.lingala.zip4j:zip4j:2.7.0"
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'
}

test {
    maxHeapSize = "3g"
    useJUnitPlatform()
    systemProperties System.properties
    outputs.upToDateWhen { false }
}

task karateDebug(type: JavaExec) {
    classpath = sourceSets.test.runtimeClasspath
    main = 'com.intuit.karate.cli.Main'
}

def defaultEncoding = 'UTF-8'
tasks.withType(AbstractCompile).each { it.options.encoding = defaultEncoding }
tasks.withType(GroovyCompile).each { it.groovyOptions.encoding = defaultEncoding }

sourceSets {
    test {
        resources {
            srcDir file('src/test/java')
            exclude '**/*.java'
        }
    }
}

karate-config.js

Karateの設定ファイルです。今回は必要最低限の設定としています。

function fn() {
  var env = karate.env; // get java system property 'karate.env'
  if (!env) {
      env = 'dev'; // a custom 'intelligent' default
  }
  karate.log('karate.env system property was:', env);

  var config = { // base config JSON
    baseUrl: 'http://127.0.0.1:18080',
  };

  // don't waste time waiting for a connection or if servers don't respond within 60 seconds
  karate.configure('connectTimeout', 60000);
  karate.configure('readTimeout', 60000);
  return config;
}

モックサーバーの実行

テスト実行用に、APIサーバー(モック)を立ち上げます。
今回用意したAPIサーバーは以下の通りです。

# mock.feature
Feature: Mock Server
  Background:
    * def userInfo =
    """
    [
      {id: 'A001', name: '太郎', result: 'success'},
      {id: 'A002', name: '二郎', result: 'error'},
      {id: 'A003', name: '三郎', result: 'error'},
    ]
    """

# -----------------------------------------------
# リクエストパラメータargの値を返すAPI
# return { "item":"(argの値)" }
# GET:/returnArgValue
# -----------------------------------------------
  Scenario: methodIs('get') && pathMatches('/returnArgValue')
    * print requestParams
    * print paramValue('arg')
    * def value = paramValue('arg')
    * def response = { item : '#(value)' }

# -----------------------------------------------
# ユーザ情報登録API
# userInfo内の該当IDに応じた結果を返す
# return { result : 'success' }
# POST:/userInfoRegister
# -----------------------------------------------
  Scenario: methodIs('post') && pathMatches('/userInfoRegister')
    * def requestBody = request
    * def id = requestBody.id
    * print 'id : ' + id
    * def target = karate.jsonPath(userInfo, "$[?(@.id=='" + id + "')]")[0]

    # userInfoに該当のものがなかった場合、エラーを返す
    * eval if (target == null) karate.set('responseStatus', 400)
    * eval if (target == null) karate.abort()

    # resultがerrorの場合、400エラーを返す
    * eval if (target.result == 'error') karate.set('responseStatus', 400)
    * eval if (responseStatus == 400) karate.abort()

    # 以外はsuccessとして正常応答を返す
    * def response = { result : 'success' }

1.リクエストパラメータの値をシンプルに返すAPI
2.ユーザ登録登録を模した簡易API
上記のモックサーバーをserver.bat。もしくは以下コマンドを実行することで起動させます。

$ java -jar karate.jar -p 18080 -m mock.feature
20:47:46.205 [main] INFO  com.intuit.karate.Main - Karate version: 0.9.5
Warning: Nashorn engine is planned to be removed from a future JDK release
20:47:47.832 [main] INFO  com.intuit.karate - backend initialized
20:47:48.699 [main] INFO  c.intuit.karate.netty.FeatureServer - server started - http://127.0.0.1:18080

正常起動すると上記のような文言が出力されます。これで準備OKです。

実装パターン8選

1.通常パターン

まずは最もシンプルなパターンです。

KarateではGiven-When-Then-And形式でテストステップを記述していきます。

  • Given

    • オブジェクトの作成や設定など、前提条件を記述する。

  • When

    • イベントやアクションを記述する。REST-APIの呼び出しなどを行う部分。

  • Then

    • 期待される結果を記述する。アサーションを使用し、実際の結果と記述した期待結果を比較する。

  • And

    • 直前のステップと同じ意味をもつ。複数の定義や条件を連続して記述する際に利用する。

この形式に則り、テストシナリオを記述していきます。

# sample.feature
Feature: Karate Sample
  Background:
    * url baseUrl

Scenario: 通常パターン
    Given path '/returnArgValue'
    And param arg = 'apple'
    When method get

    Then status 200
    And match response == '#object'
    And match response.item == 'apple'

このシナリオでは、リクエストパラメータargの値を返すAPIを呼び出し、そのレスポンスデータに対しアサーションを行っています。

流れとしては
(1)http://127.0.0.1:18080/returnArgValue のAPIをテストする
(2) argパラメータに'apple'を指定
(3) HTTP GETでアクセス
(4) HTTPステータス200 であることを確認
(5) レスポンスデータがオブジェクトであることを確認
(6) jsonデータ(item)の値が apple であることを確認
となります。

では実際にテスト実行してみましょう。

テスト実行する際には、以下のようなシナリオ実行用のランナークラスを用意します。Karate.run()には実行したいfeatureファイルを直接指定することもできますし、指定しない場合は、パッケージ内(scenariosパッケージ)のテストが実行されます。
今回は、指定なしで呼び出します。

# ScenarioRunner
package scenarios;
import com.intuit.karate.Results;
import com.intuit.karate.Runner;
import com.intuit.karate.junit5.Karate;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ScenarioRunner {
    @Karate.Test
    Karate run() { return Karate.run().relativeTo(getClass()); }
}

コンソールで以下gradleコマンドを実行します。(※windowsOS上で実行しています)

$ gradlew clean test --tests "scenarios.ScenarioRunner.run"

下記のような文言が表示されれば成功です。

Welcome to Gradle 6.5!

Here are the highlights of this release:
 - Experimental file-system watching
 - Improved version ordering
 - New samples

For more details see https://docs.gradle.org/6.5/release-notes.html

Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details

Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/6.5/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 1m 20s
4 actionable tasks: 4 executed

またKarateでは、以下のようなレポートが出力され、テスト実行内容を確認できます。
build/karate-reports配下にscenarios.sample.htmlが出力されるのでブラウザなどで表示してみましょう。

2.データ駆動型テスト(DDT)

2つ目はデータ駆動型テストです。
同じテストシナリオを、実行するデータのみを変えてテストしたいときに効果的です。
さっそく試していきましょう。
まずは、テストデータ3件あるデータファイルを準備します。

#testData.json
[
  {"pattern":"A", "data":"apple"},
  {"pattern":"A", "data":"grape"},
  {"pattern":"B", "data":"strawberry"}
]

次にテストシナリオを用意していきます。

# sample.feature
Feature: Karate Sample
  Background:
    * url baseUrl
    * def dataSet = read('data/testData.json')

  Scenario Outline: データ駆動パターン [<data>]
    Given path '/returnArgValue'
    And param arg = '<data>'
    When method get

    Then status 200
    And match response == '#object'
    And match response.item == '<data>'

    Examples:
      | dataSet |

データ駆動として実装する方法ポイントは3点。
1)テストデータを読み込む
  read('data/testData.json')で読み込みたいファイルを指定します
2)Scenario Outline を定義する
3)Examplesにテストデータ(dataSet)を定義する
  今回は、testData.jsonを読み込ませテストデータとして指定する形を取っていますが
  以下ように、列ごとに値を直接定義することも可能です。

Examples:
    | name   | country  |
    | monza  | Italy    |
    | spa    | Belgium  |
    | sepang | Malaysia |

では実際に実行してみます。(※実行方法はパターン1同様)
Karateレポートを確認するとデータごとにテストできていることがわかりますね。

3.JSONPathを使ったデータ抽出

3つ目はデータ駆動テストと相性が良い、データ抽出処理です。
パターンごとにテストデータを選別したい時に良く利用します。
今回は、同一のデータファイル内でパターンに分けてテストデータを用意してみます。
もちろんパターン毎にデータファイルを分ける方法もありますので、状況に合わせ参考にしてみてください。

テストデータに「pattern」を設けAとBに分けておきます。

#testData.json
[
  {"pattern":"A", "data":"apple"},
  {"pattern":"A", "data":"grape"},
  {"pattern":"B", "data":"strawberry"}
]

テストシナリオ側では、Karateのgetキーワードを利用しJsonPathでデータフィルタリングを行います。
データセットの変数が用意できれば、あとはExamplesに指定してパターンごとのテストが行えます。

# sample.feature
Feature: Karate Sample
  Background:
    * url baseUrl
    * def dataSet = read('data/testData.json')
    * def patternA_DataSet = get dataSet $[?(@.pattern == 'A')]
    * def patternB_DataSet = get dataSet $[?(@.pattern == 'B')]

Scenario Outline: データ駆動パターンA [<data>]
    Given path '/returnArgValue'
    And param arg = '<data>'
    When method get

    Then status 200
    And match response == '#object'
    And match response.item == '<data>'

    Examples:
      | patternA_DataSet |

4.待機処理

4つ目は待機処理です。
テスト実行時、待機処理を入れるパターンは意外に多いです。
例えばA処理の後、一定時間待機した後、B処理を行いたいといった場合などですね。
では実装していきましょう。

Scenario: 待機処理
    * def sleep =
      """
        function(pause){ java.lang.Thread.sleep(pause) }
      """
    Given path '/returnArgValue'
    And param arg = 'apple'
    When method get

    Then status 200
    And match response == '#object'
    And match response.item == 'apple'

    # 待機処理
    * def temp = sleep(10000);

    Given path '/returnArgValue'
    And param arg = 'banana'
    When method get

    Then status 200
    And match response == '#object'
    And match response.item == 'apple'

待機処理を行うには
・スリープ処理用のJavaScript Functionsを用意する
・定義した変数(sleep)を呼び出す
で行います。 このサンプルでは10秒待機させています。

続きは後編でお届けします

少し長くなりそうでしたのでここまでを前編としたいと思います。 後編では、残りの4パターンをご紹介いていきます!

執筆者プロフィール:柏木 雄介
医療分野での業務システム開発・導入経験を経て、2020年にSHIFTへ入社。
QA活動やテスト自動化を担当しながら、アーキテクト目指して日々奮闘中。
趣味は、ゲームとサッカー(ジュビロ磐田サポ)。
最近ハマっているのは洗車。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/