2
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?

Elasticsearch クエリが正しく動くことをテストで保証できているか?Mutation Testing CLI を作った

2
Last updated at Posted at 2026-04-16

課題

昨今、AI がテストコードを自動生成するケースが増えています。開発速度は上がる一方で、「そのテスト自体の品質が十分かどうか」を確認することは難しくなってきました。

ここで言う「テストの品質」とは、Elasticsearch クエリに対するテストが、クエリの変化を正しく検知できるかどうかです。テストが存在していても、それがクエリのセマンティクスを十分に検証できていなければ、クエリの品質は保証されません。

Elasticsearch のクエリはパラメータひとつの変化が検索結果に大きく影響します。MustShould に変えれば返却ドキュメントの集合が変わり、スコアリング句の変更は並び順を変えます。こうした変化を検知できるテストを書くには、テストデータと検証内容の両方が十分である必要があります。

  • テストデータ: クエリ条件を満たさないドキュメントを含めておかないと、クエリを変えても返却結果が変わらず、変化に気づけません。
  • アサーション: 「結果が返ってくること」だけでなく、返ってきたドキュメントの内容や順序まで検証しないと、スコアリングの変化を見逃してしまいます。

つまり、「テストはあるが、クエリの変化を検知できていない」という状態が起こりえます。このようなテストの品質不足を定量的に可視化したいと思い、ツールを作ることにしました。

今回作ったツール esmutantとは

esmutantgo-elasticsearchのTyped APIで書かれたElasitcsearchクエリに対してMutation Testingを行うCLIツールです。

このツールが行うことはシンプルで、

  1. クエリ構築コードを解析して変異対象の箇所を特定する
  2. Must → nilFilter → Must のような小さな変異を加えたコードを生成する
  3. 変異後のコードで go test を実行し、テストが失敗するかを確認する
  4. 結果を Mutation Score としてレポートする
Mutation Score: 9/11 (81.8%)
  Score = Killed / (Killed + Survived + Timeouts + Errors)
  Skipped mutants are excluded from the score.
  Killed: 9  Survived: 2  Timeouts: 0  Errors: 0  |  Skipped: 4

KILLED (9):
  search.go:41 BuildActiveUsersQuery    BoolQuery.Must → nil    [RemoveClause]
    Detected by:
      TestBuildActiveUsersQuery  ✗ failed

SURVIVED (2):
  search.go:98 BuildArticlesQuery    BoolQuery.Must → Should    [MustToShould]
    Tested by (all passed):
      TestBuildArticlesQuery  ✓ passed
      TestBuildUserByEmailQuery  ✓ passed

Test Contribution Summary:
  4 test case(s) ran / 11 mutant(s) evaluated

  Tests that killed no mutants (may not test query logic):
    TestHealthCheck

Killed はテストが変異を検知できた(テストが失敗した)ことを意味し、Survived はテストが変異を見逃した(全テストがパスした)ことを意味します。Survived の行を見ることで「どのクエリのどの部分に対して、アサーションが不足しているか」を具体的に把握できます。

Mutation Testing とは

Mutation Testing は、プログラムに小さな変異(Mutation)を意図的に加え、テストスイートがその変異を検知できるかを測定することでテストの品質を評価する手法です。歴史ある手法で、近年は CI に組み込む事例も増えています。

変異を加えたコード(Mutant)に対してテストを実行し、結果を以下のように分類します。

結果 意味
Killed テストが失敗した → 変異を検知できた
Survived テストが全てパスした → 変異を見逃した

Mutation Score は次の式で計算されます。

Mutation Score = Killed / (Killed + Survived + Timeouts + Errors) × 100

スコアが高いほど、テストがコードの変化に対して敏感であることを意味します。

esmutantの設計思想

go-elasticsearch Typed API に特化する

Elasticsearch クライアントには DSL を文字列や map[string]interface{} で組み立てるアプローチと、go-elasticsearch v8 の Typed API のように型安全に組み立てるアプローチがあります。

esmutant は後者の Typed API に特化しています。型情報が明示的なため、go/packages で型情報付きのパッケージ解析を行い、github.com/elastic/go-elasticsearch/v8/typedapi/types パッケージの構造体フィールドであることを型レベルで確認した上で変異対象を特定できます。これにより、同名フィールドを持つ他の構造体を誤って変異対象にしてしまうことを防いでいます。

元ファイルを変更しない

go test -overlay を使うことで、元のソースファイルを一切変更せずに変異後のコードでテストを実行できます。-overlay{Replace: {"元ファイルのパス": "変異後ファイルのパス"}} という JSON を渡すことでファイルを仮想的に差し替える仕組みです。

go test -json -overlay /tmp/overlay.json -count=1 ./...

実際の Elasticsearch に対してテストを実行する

モックやスタブを使わず、実際の Elasticsearch インスタンスに対してテストを実行します。クエリの変異がドキュメントの返却結果に影響するかどうかは、実際に Elasticsearch を動かしてみなければわからないからです。

技術スタック

技術 用途
go/ast go/parser go/token ソースコードの AST 解析・フィールドの書き換え
go/packages 型情報付きパッケージ読み込み(ES 型の識別)
go test -overlay 元ファイルを変更せずに変異後コードでテスト実行
go test -json テスト結果を構造化パース(どのテスト関数が検知したかを追跡)

サポートしている Mutation Operators

現在 9 種類のオペレーターを実装しています。

BoolQuery 系

オペレーター 変異内容
RemoveClause Must / Shouldnil
MustToShould MustShould(必須条件をオプション条件に)
ShouldToFilter ShouldFilter(スコアリング句を非スコアリングに)
FilterToMust FilterMust(非スコアリングをスコアリング必須に)
RemoveMustNot MustNotnil(除外条件を削除)

Range クエリ系

オペレーター 変異内容
RangeBoundary GteGtLteLt(包含境界を排他境界に)
RangeDirection GteLteGtLt(上下限を入れ替え)

その他

オペレーター 変異内容
RemoveFunctionScoreFilter FunctionScore.Filternil(ブーストスコープを全ドキュメントに拡大)
MultiMatchType BestfieldsPhrase / Mostfields

使い方

インストール

go install github.com/kurakura967/go-elasticsearch-mutant/cmd/esmutant@latest

基本的な実行

esmutant run ./...

テストコードが別パッケージにある場合は --test フラグで指定します。

esmutant run ./internal/repository/... \
  --test ./testing/integration/... \
  --workers 1

主なフラグ

フラグ デフォルト 説明
--test (対象パターンと同じ) テスト実行パッケージを別指定
--workers 1 並列ワーカー数
--threshold 0(無効) Mutation Score の最低ライン(超えなければ exit 1)
--verbose false Survived / Error の go test 出力を表示

--workers の注意点

統合テストで共有の Elasticsearch インデックスを使っている場合、--workers を 2 以上にすると複数ワーカーが同一インデックスを同時に操作して競合が発生し、偽陽性(本来 Survived のはずが Killed と判定される)が生じます。

共有インデックスを使う統合テストでは --workers 1 を推奨します。

今後の展望

  • 等価変異(Equivalent Mutation)への対応: MatchAll に対する Filter → Must のように、どんなテストを書いても原理的に検知できない変異を「等価変異の可能性あり」として注記する機能
  • CI への組み込みガイド: --threshold を使った品質ゲートの設定例
  • オペレーターの拡充: プロダクトでの使用経験をもとに、検知価値の高い変異パターンを追加していく
2
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
2
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?