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)
コードブロックでは、以下のことを行っています
- GraphQL の mutation を実行して 受注データ を登録
- レスポンスから
slo_no(受注番号)とslo_rev(リビジョン番号)を取り出してdefする
Karate の def は、呼び出し元から見ると「戻り値」扱いになります。
後で call / callonce からこの feature を呼ぶと、
reg.slo_noreg.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')
としているのがポイントです。
理由:
-
callonceにすることでJVM プロセス内でこの登録 feature が 1 回だけ 実行されます。
- 登録処理が 検索 feature の Background に書かれている ので検索シナリオの前に必ず登録が終わります。
- 戻り値の
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 を実行すると、
- Runner が
SalesOrder_Ins01.featureをテストとして実行 - さらに
SalesOrder_Srch01.featureのcallonceからも呼び出される
という流れで、登録が 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.featureSalesOrder_Ins01_Canceled.featureSalesOrder_Ins01_Approved.feature
のようにパターン別の登録 feature を用意し、それぞれから slo_no / slo_rev を export する形にしておけば、検索側で必要なパターンだけを callonce して使い分けられます。
まとめ
Karate × GraphQL の E2E テストで
- 「登録してから検索する」
- 「でもテスト実行順序に振り回されたくない」
- 「ジョブ実行時に登録が重複実行されるのは避けたい」
という要件に対して、
- 登録 feature で キー項目 を
defして export - 検索 feature の Background から
callonceで登録 feature を呼び出す - Runner (
GraphQLTestRunner.java) では@helperタグを除外し、helper feature をトップレベル実行から外す - 必要に応じて
parallel(1)でテスト全体も順次実行
という構成にすることで、順序制御を テストコードとして明示的に表現できます。
GraphQLのE2Eテストで順序制御を実現したい方の参考になれば幸いです。