1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphQL の E2E テストを Karate で書いていると、

  • まず 受注データ(SalesOrder)を登録(mutation) してデータを作る
  • そのデータをキーにして 受注データ を検索(query) する

という「登録 → 検索」の流れをテストしたくなる場面がよくあります。

ただ、このときにこんな問題が出てきます。

  • 検索テストは「登録が終わってから」じゃないと成立しない
  • CI / ジョブ実行時に 登録用 feature が 2 回実行されてしまうことがある
  • feature ファイルの並び順や Runner の指定順に依存したくない

この記事では、自分が実際にやった構成を例に、

Karate × GraphQL で
「登録 → 検索」の順序を
コード上で明示的に制御するパターン

を紹介します。

全体構成

サンプルのファイル構成はざっくりこんな感じです。

  • SalesOrder_Ins01.feature :受注データの登録(GraphQL mutation)
  • SalesOrder_Srch01.feature :受注データの検索(GraphQL query)
  • GraphQLTestRunner.java :Karate の JUnit Runner

ポイントは以下のとおりです。

  • 登録 feature が 「検索の前に必ず 1 回だけ」 実行される
  • 登録 feature を単体で実行して確認することもできる
  • CI / ジョブ実行では 登録 feature が重複実行されない

これを Karate の機能だけで実現します。

1. 登録 feature:テストデータ生成 + 受注番号の export

まず、受注データ 登録用の feature です。
feature 自体もテストとして動きますが、他の feature から “呼び出される” 前提で作ります。

@helper
Feature: SalesOrder_Ins01 受注データ登録

  Background:
    * def config = karate.callSingle('classpath:karate-config.js')
    * url config.baseUrl
    * headers config.headers
    * def query = read('./SalesOrder_Ins01.graphql')

  Scenario: Execute SalesOrder_Ins01 mutation
    Given request
      """
      {
        "query": #(query),
        "variables": {
          "input": {
            // ここに登録用の入力パラメータを定義
          }
        }
      }
      """
    When method post
    Then status 200

    # (必要に応じてレスポンス全体の検証も入れる)
    # * def expectedResponse = read('./SalesOrder_Ins01_expectedResponse.json')
    # And match response.data.SalesOrder_Ins01 == expectedResponse.data.SalesOrder_Ins01

    # 返却用:SalesOrder のキー情報を抽出
    * def slo_no  = response.data.SalesOrder_Ins01.down0.slo_no
    * def slo_rev = response.data.SalesOrder_Ins01.down0.slo_rev
    * karate.log('slo_no:', slo_no, 'slo_rev:', slo_rev)

コードブロックでは、以下のことを行っています

  1. GraphQL の mutation を実行して 受注データ を登録
  2. レスポンスから slo_no (受注番号)と slo_rev (リビジョン番号)を取り出して def する

Karate の def は、呼び出し元から見ると「戻り値」扱いになります。
後で call / callonce からこの feature を呼ぶと、

  • reg.slo_no
  • reg.slo_rev

のようにして値を参照できます。

また、feature 先頭に @helper タグを付けています。
これは Runner 側で、

テストスイートとしては直接は回さないが、他の feature からは呼び出したい

という区別をするためのフラグとして使います。

なお、karate.callSingle を使用しているのは、設定ファイルが複数の Feature やスレッド間で共有されるシングルトンとして機能し、テストの実行単位に依存しない一貫した設定を保証するためです。

2. 検索 feature:Background で「登録 feature を callonce する」

次に、受注データ 検索側の feature です。
ここが 順序制御のコアです。

Feature: SalesOrder_Srch01 受注データの検索

  Background:
    * def config = karate.callSingle('classpath:karate-config.js')
    * url config.baseUrl
    * headers config.headers

    # 検索用 GraphQL クエリ
    * def query = read('./SalesOrder_Srch01.graphql')

    # SalesOrder 登録を一度だけ実行して、slo_no / slo_rev を取得
    * def reg = callonce read('./SalesOrder_Ins01.feature')
    * def slo_no  = reg.slo_no
    * def slo_rev = reg.slo_rev
    * print 'slo_no:', slo_no
    * print 'slo_rev:', slo_rev

  Scenario: Execute SalesOrder_Srch01 query
    Given request
      """
      {
        "query": #(query),
        "variables": {
          "input": {
            "slo_no":  #(slo_no),
            "slo_rev": #(slo_rev)
          }
        }
      }
      """
    When method post
    Then status 200

    # (必要に応じてレスポンス比較)
    # * def expectedResponse = read('./SalesOrder_Srch01_expectedResponse.json')
    # And match response.data.SalesOrder_Srch01 == expectedResponse.data.SalesOrder_Srch01

ここが順序制御の本質

Background の中で* def reg = callonce read('./SalesOrder_Ins01.feature')
としているのがポイントです。

理由:

  1. callonce にすることでJVM プロセス内でこの登録 feature が 1 回だけ 実行されます。
     
  2. 登録処理が 検索 feature の Background に書かれている ので検索シナリオの前に必ず登録が終わります。
     
  3. 戻り値の reg から slo_no / slo_rev (受注番号 / リビジョン番号)を取り出し、後続のGraphQL の variables.input にそのまま渡せます。

これで、

  • 「検索テストを実行するときには、必ず事前に 登録が 1 回だけ実行される」

という状態を、Karate の構文だけで保証できます。

3. Runner の設定: テストスイートからの除外と実行制御

JUnit から Karate を実行している Runner 側はこんな感じです。

package graphql;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

import com.intuit.karate.Results;
import com.intuit.karate.Runner;

class GraphQLTestRunner {

    @Test
    void testParallel() {
        Results results = Runner.path("classpath:graphql_run")
                                .tags("~@helper")
                                .parallel(1);

        assertEquals(0, results.getFailCount(), results.getErrorMessages());
    }

}

ここでのポイントは 2 つです。

3-1. tags("~@helper") で helper feature をテストスイートから除外

  • @helper が付いている SalesOrder_Ins01.feature は、Runner 実行時には トップレベルのテストケースとしては実行されません
  • その代わり、callonce検索 feature からだけ呼び出される 形にしています

もし ~@helper を付けずに Runner を実行すると、

  1. Runner が SalesOrder_Ins01.feature をテストとして実行
  2. さらに SalesOrder_Srch01.featurecallonce からも呼び出される

という流れで、登録が 2 回実行されてしまう可能性があります。

それを防ぐために、

  • 「登録 feature は helper 扱い
  • テストスイート全体としては直接は実行しない

という整理をしています。

3-2. parallel(1) でテスト全体も順次実行

今回は DB に対する登録 → 検索 というシナリオなので、

  • テスト全体も 1 スレッドで順次実行parallel(1)

にしています。

テストデータや環境を完全に分離できていれば parallel(n) も検討できますが、

  • 共通 DB を使う
  • 既存システムと連携する

といった事情がある場合、まずは parallel(1)順序の安定性を優先する方が安全です。

4. この構成で何が嬉しいか(順序制御の観点でのメリット)

✅ 検索より前に登録が終わっていることが、コードで保証される

  • 「SalesOrder_Ins01.feature を先に実行しておいてね」といった口頭・ドキュメントベースの前提が不要になります
  • 依存関係は SalesOrder_Srch01.feature の Background に明示されているので、後から見ても意図が分かりやすいです

✅ テストデータ生成ロジックを 1 箇所に集約できる

  • 登録処理のロジックは SalesOrder_Ins01.feature に集中できます
  • 複数の検索シナリオ・検索 feature から callonce で再利用できます
  • 登録仕様に変更があったとき、修正箇所はこの 1 ファイルで済みます

✅ CI / ジョブ実行で「登録が 2 回実行される問題」を防げる

  • Runner では @helper を除外しているので、SalesOrder 登録は「呼び出し側からの 1 回きり」 になります
  • 「登録だけ単体で確認したい」ときは、IDE などからSalesOrder_Ins01.feature を直接実行すれば OK、という運用にできます

5. 応用アイデア(順序制御を広げる)

順序制御という観点で、応用するとこんな使い方もできます。

5-1. 複数テストから同じ 登録 feature を共有

別パターンの検索・一覧表示・エクスポートなどが増えても、

  • 各検索系 feature の Background から同じ* def reg = callonce read('./SalesOrder_Ins01.feature')を書くだけで流用可能です

1 テストスイート中では callonce のおかげで 登録は 1 回だけなので、不要にテストデータを量産せずに済みます。

5-2. データパターンを増やしたい場合は feature を分割

「標準パターン」「キャンセル済みパターン」「承認済みパターン」などが必要なら、

  • SalesOrder_Ins01_Standard.feature
  • SalesOrder_Ins01_Canceled.feature
  • SalesOrder_Ins01_Approved.feature

のようにパターン別の登録 feature を用意し、それぞれから slo_no / slo_rev を export する形にしておけば、検索側で必要なパターンだけを callonce して使い分けられます。

まとめ

Karate × GraphQL の E2E テストで

  • 「登録してから検索する」
  • 「でもテスト実行順序に振り回されたくない」
  • 「ジョブ実行時に登録が重複実行されるのは避けたい」

という要件に対して、

  1. 登録 feature で キー項目 を def して export
  2. 検索 feature の Background から callonce で登録 feature を呼び出す
  3. Runner (GraphQLTestRunner.java) では @helper タグを除外し、helper feature をトップレベル実行から外す
  4. 必要に応じて parallel(1) でテスト全体も順次実行

という構成にすることで、順序制御を テストコードとして明示的に表現できます。
GraphQLのE2Eテストで順序制御を実現したい方の参考になれば幸いです。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?