assertion-quality について
テストのアサーションの多様性と深さを評価するスキルです。テストが複数の振る舞いを検証しているか確認する開発者向けに、アサーションを分類し、アサーションのないテストや状態・構造の検証などを集計します。
主な機能
- 01テストアサーションの識別と分類
- 02テストごとの件数とカテゴリ集計
- 03アサーションカテゴリの分布集計
- 04アサーションなし・浅いテストの検出
- 05否定・状態・構造の検証分析
活用シーン
- 01戻り値だけを検証するテストの確認
- 02アサーションのないテストの発見
- 03テストスイートの多様性評価
- 04生成したテストの完了前レビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill assertion-qualityインストール後、Claude Code で/assertion-qualityを入力して読み込まれたか確認します。
詳細
アサーションの多様性分析
サポート対象のあらゆる言語のテストコードを分析し、アサーションの多様性と有意義さを測定します。テストが「出力が X と等しい」ことを確認するだけでなく、構造、例外、状態遷移、副作用、不変条件など、正しさのさまざまな側面を検証しているかどうかを明らかにするメトリクスレポートを作成します。
言語固有のガイダンス:呼び出し元から提供された、または実行時に一覧表示された
test-analysis-extensionsカタログを読み、その後、対応する言語ファイル (dotnet.md、python.md、typescript.md、go.mdなど)を読みます。test-analysis-extensionsは参照専用です。スキルとして呼び出さないでください。 バンドルが存在しない場合は、固定されたフレームワークとこのスキルのルールを使用し、 参照が見つからないことを報告して、インストールディレクトリを検索しないでください。
アサーションの多様性が重要な理由
アサーションの多様性が低い場合、テストが浅い可能性があります。アサーションされていないロジックにバグが潜んだまま、テストが通ることがあります。よくある兆候は次のとおりです。
| 問題 | 兆候 | 影響 |
|---|---|---|
| 些細なアサーション | テストに Assert.IsNotNull(result) / assert result is not None / expect(x).toBeDefined() しか含まれていない | テストは通りますが、正しさを検証できていません |
| 単一の値への執着 | 常に1つのフィールドまたは戻り値だけを確認する | アサーションされていないロジックのバグを見逃します |
| 否定アサーションがない | 起きてはならないことを確認しない | 偽陽性によるリグレッションが入り込みます |
| 状態の確認がない | オブジェクトの状態変化を検証しない | 副作用やライフサイクルの問題を見逃します |
| 構造の確認がない | 最上位の値しかアサーションしない | ネストされたオブジェクトのバグに気づきません |
| アサーションのないテスト | 呼び出しはするが検証しないテスト | コードカバレッジが実態を反映せず、誤った安心感を与えます |
使用する場面
- アサーションの品質や深さを評価するよう求められた場合
- 「テストは本当に意味のあることを検証していますか?」と尋ねられた場合
- テストのアサーションが浅すぎる、または些細すぎるかどうかを知りたい場合
- アサーションのカバレッジメトリクスや多様性分析を求められた場合
- テストが通っていても誤った安心感を与えていると疑われる場合
test-engineerエージェント(またはテスト生成ワークフロー)が、実行完了を宣言する前に、生成したばかりのテストを自己レビューする手順としてこのスキルを呼び出す場合
使用しない場面
- 新しいテストを書きたい場合(任意の言語には
code-testing、MSTest に限る場合はwriting-mstest-testsを使用) - アサーション以外のアンチパターンを検出したい場合(
test-anti-patternsを使用) - アサーションを修正または書き直したい場合(直接サポートしてください)
- コードカバレッジ率について尋ねられた場合(対象外です。この分析では行カバレッジではなく、アサーションの品質を分析します)
入力
| 入力 | 必須 | 説明 |
|---|---|---|
| テストコード | はい | 分析対象となる1つ以上のテストファイル、またはテストプロジェクトのディレクトリ |
| プロダクションコード | いいえ | アサーションが重要な動作をカバーしているか評価するための、テスト対象のコード |
ワークフロー
ステップ1:言語を特定して拡張ファイルを読み込む
言語とテストフレームワークを特定します。提供された test-analysis-extensions カタログを基準に、対応する extensions/ 内のファイルを読みます。他の言語のファイル名を調べる際は、そのカタログを使用します。必要な場合に限り、その既知のディレクトリのみを確認します。参照が利用できない場合は、確認できる根拠の範囲が変わるだけであり、ワークスペースのルートや依頼されたレビューの範囲は変わりません。
ステップ2:テストコードを収集する
ユーザーから提供されたすべてのテストファイルを読みます。ユーザーがディレクトリまたはプロジェクトを指定した場合は、言語拡張ファイルのマーカー(例:MSTest の [TestMethod]、pytest の def test_*、Jest の it() / test()、Go の func TestXxx)を使って、すべてのテストファイルをスキャンします。
ステップ 3:すべてのアサーションを分類する
各テストメソッドについて、すべてのアサーションを特定し、言語に依存しない次のカテゴリに分類します。
| カテゴリ | 検証内容 | 言語ごとの例 |
|---|---|---|
| 等価性 | 戻り値が期待値と一致する | Assert.AreEqual(MSTest)、Assert.Equal(xUnit)、assert x == y(pytest)、expect(x).toBe(y)(Jest)、assertEquals(JUnit)、if got != want { t.Error... } / assert.Equal(t, want, got)(Go)、x shouldBe y(Kotest)、Should -Be(Pester)、EXPECT_EQ(GoogleTest) |
| ブール値 | 条件が成立する | Assert.IsTrue、assert flag(Python)、expect(x).toBeTruthy()(Jest)、assertTrue(JUnit)、assert.True(t, ok)(testify)、x.shouldBeTrue()(Kotest)、Should -BeTrue(Pester)、EXPECT_TRUE |
| Null / None / Nil | 値の有無 | Assert.IsNull(.NET)、assert x is None(pytest)、expect(x).toBeNull()(Jest)、assertNull(JUnit)、assert.Nil(t, v)(testify)、XCTAssertNil(XCTest)、Should -BeNullOrEmpty(Pester) |
| 例外 / エラー | エラー処理の動作 | Assert.Throws<T>()、pytest.raises(E)、expect(fn).toThrow(E)、assertThrows<E>、assert.Error(t, err) / assert.ErrorIs、#[should_panic](Rust)、XCTAssertThrowsError、Should -Throw、EXPECT_THROW |
| 型チェック | 実行時の型の正確性 | Assert.IsInstanceOfType、assert isinstance(x, T)、expect(x).toBeInstanceOf(T)、assertInstanceOf、assert.IsType(t, T{}, v)、assert!(matches!(value, Pattern))(Rust)、Should -BeOfType |
| 文字列 | テキストの内容と形式 | StringAssert.Contains、assert sub in s、expect(s).toMatch(/x/)、assertTrue(s.contains(...))、assert.Contains(t, s, sub)、s shouldContain sub、Should -Match、EXPECT_THAT(s, HasSubstr(...)) |
| コレクション | コレクションの内容と構造 | CollectionAssert.Contains、assert item in collection、expect(arr).toContain(x)、assertIterableEquals、assert.Contains(t, slice, item)、col shouldContainExactly listOf(...)、Should -Contain、EXPECT_THAT(c, ElementsAre(...)) |
| 比較 | 順序と大きさ | Assert.IsTrue(x > y)、Is.GreaterThan、assert x > y、expect(x).toBeGreaterThan(y)、assertTrue(x > y)、assert.Greater(t, x, y)(testify) |
| 近似 | 浮動小数点数または許容誤差に基づく判定 | Assert.AreEqual(expected, actual, delta)、pytest.approx(y)、expect(x).toBeCloseTo(y)、assertEquals(x, y, delta)、assert.InDelta(t, x, y, delta)、EXPECT_NEAR、EXPECT_DOUBLE_EQ |
| 否定 | 起きてはならないこと | Assert.AreNotEqual、assert x != y、expect(x).not.toBe(y)、assertNotEquals、assert.NotEqual(t, x, y)、refute(Minitest / Ruby)、Should -Not -Be |
| 状態 / 副作用 | 状態の遷移と副作用 | 変更後のオブジェクトプロパティに対するアサーション、モック呼び出しの検証:mock.Verify(...)(Moq)、mock_method.assert_called_with(...)(Python unittest.mock)、expect(mock).toHaveBeenCalledWith(...)(Jest)、verify(mock).method(...)(Mockito)、Should -Invoke(Pester)、expect { code }.to change(obj, :attr)(RSpec) |
| 構造 / ディープ | オブジェクトの深い階層まで含めた正確性 | リッチ等価性型を使った Assert.AreEqual、assertThat(obj).usingRecursiveComparison()(AssertJ)、.toEqual({...})(Jest のディープ等価性)、cmp.Diff(Go go-cmp)、スナップショットテスト(.toMatchSnapshot()、syrupy、SnapshotTesting)、assertThat(col).extracting(...)(AssertJ のチェーン) |
1 つのアサーションが複数のカテゴリに該当することがあります(例:Assert.AreNotEqual は「等価性」と「否定」の両方に該当し、expect(mock).toHaveBeenCalledWith(...) は「状態 / 副作用」と特定の呼び出しに関するアサーションの両方に該当します)。
読み込まれた言語拡張ファイルを参照して、フレームワーク固有のアサーション API の正確な一覧を確認してください。
ステップ 4:指標を算出する
テストスイートの次の指標を算出します。
テストごとの指標
- アサーション数:各テストメソッドのアサーション数
- アサーションのカテゴリ:各テストで使用されるカテゴリ
スイート全体の指標
- テストごとの平均アサーション数:アサーションの総数 / テストメソッドの総数
- アサーションタイプの広がり:スイート全体で使用される異なるアサーションカテゴリの数(全12カテゴリ中)
- アサーションが0件のテスト:アサーションがまったくないテストメソッドの数と割合
- 単純なアサーションのみを含むテスト:すべてのアサーションが null チェックまたは
Assert.IsTrue(true)のみであるテストの数と割合 — 単純なアサーションとは、意味のある値の検証を行わないものです - 自己参照アサーションを含むテスト:入力値と、その値を往復変換または恒等変換した値を比較するアサーション(例:
Assert.AreEqual(input, Parse(input.ToString())))、またはフィールドを自身と比較するアサーション(例:Assert.AreEqual(dto.Name, dto.Name))を含むテストの数と割合。これらはトートロジーであり、動作ではなく配線を検証しています。 - 否定アサーションを含むテスト:数と割合(目標:テストの少なくとも10%で、起きてはならないことを検証する)
- 例外アサーションを含むテスト:数と割合
- 状態/副作用アサーションを含むテスト:数と割合
- 構造/深いアサーションを含むテスト:数と割合
- 単一カテゴリのテスト:アサーションカテゴリを1つだけ使用するテストの数と割合
ステップ 5:調整ルールを適用する
報告する前に、調査結果を調整します:
- マッチャーの述語を評価してから、その弱点を説明してください。 弱いと判断する各アサーションについて、その正確な述語を満たしてしまう現実的な不具合のある値または動作を1つ挙げてください。意味があると評価する各アサーションについては、どの動作を検証しているかを述べてください。テストと利用可能な本番環境の契約から反例を示せない場合は、そのアサーションが弱いと推測しないでください。
- 本当に自明なものだけを「自明」とします。 null/None/nil のチェックだけなら自明です(
Assert.IsNotNull(result)、assert result is not None、expect(x).toBeDefined())。ただし、null チェックの後に意味のある値のアサーションが続く場合、自明ではありません。null チェックは本来のアサーションの前段階となるガードです。意味のある値のアサーションがないテストだけを「自明」と判定してください。 - Jest の正確なセマンティクスを使用してください。
toBeDefined()が拒否するのはundefinedだけです。nullも条件を満たしますが、nullが契約違反となる現実的な結果である場合に限り、その点に言及してください。toMatchObject(expected)は、期待する部分集合を構造的に検証します。オブジェクトの同一性やオブジェクト全体の等価性を証明するものではありません。そのような主張は決してしないでください。 - 意味のある条件を検証するブール値アサーションは自明ではありません。
Assert.IsTrue(result.IsValid)/assert result.is_valid/expect(result.isValid).toBe(true)は特定のプロパティを検証するブール値アサーションであり、自明ではありません。常に真となるアサーション(Assert.IsTrue(true)、assert True、expect(true).toBe(true))は自明です。 - 正確な構築とマッピングの検証には意味があります。 オブジェクトを構築し、要求された各プロパティにそれぞれ独立した期待値リテラルを指定するテストは、値の入れ替わり、欠落、誤った割り当てを検出できます。実装がコンストラクター、レコード、プロパティマッピング、またはインメモリストアであるというだけで、評価を下げないでください。
- テストの意図を考慮してください。 void メソッドのテストで、依存先の状態変化を検証するのは正当です。ブール値アサーションが1つだけでも問題ありません。
- 例外テストは、アサーション数が少ないのが本質です。
Assert.ThrowsException<T>(() => ...)/with pytest.raises(E): .../expect(fn).toThrow(E)/#[should_panic]だけが唯一のアサーションでも問題ありません。例外に焦点を当てたテストのアサーション数が少ないことを理由に、評価を下げないでください。 - モック呼び出しの検証と、簡潔なアサーション形式も数に含めてください。
verify(mock).method(...)(Mockito)、expect(mock).toHaveBeenCalledWith(...)(Jest)、Should -Invoke(Pester)、bare assert(pytest)、if got != want { t.Errorf(...) }(Go)は、それぞれ適切なカテゴリの実際のアサーションとして扱ってください。テストフレームワークの API が欠けているかのように扱わないでください。 - スナップショットアサーション(
.toMatchSnapshot()、syrupy、SnapshotTesting)は、構造/ディープアサーションとして数えてください。古いまま、または一度も更新されていないスナップショットは、別途指摘してください。 - プロパティベーステスト(Hypothesis の
@given、proptest!、Kotest のforAll)では、生成されたケースによってアサーションが暗黙的に実行されます。外側の枠組みではなく、内側のアサーションロジックを数えてください。 - 多様性と量を混同しないでください。 等価性アサーションが20個あるテストは量は多いものの、多様性は低いです。等価性アサーション、null チェック、例外アサーションが各1つのテストは量が少なくても、多様性は高いといえます。
- 自己参照的なアサーションは、意味のある等価性チェックではありません。 出力が入力と一致すると検証するラウンドトリップは、実際の等価性アサーションに見えますが、テスト対象の処理に恒等性が期待される場合は同語反復になります。通常の等価性アサーションとは別に指摘してください。テストの目的がラウンドトリップ(シリアライズ/デシリアライズ、エンコード/デコード)の検証である場合、そのアサーションは有効ですが、変換を試せる、単純ではない入力に対するアサーションも含めてください。
- 推奨事項は、明示された動作に対応させてください。 フォーマットのテストでは正確なフォーマット結果を固定し、検証テストでは拒否される入力を用意し、ラウンドトリップテストではエスケープ、null/空値の処理、その他の変換境界を試す入力が必要です。アサーションのない各 create/update/delete 操作には、汎用的な「状態を確認する」という対処法ではなく、その操作に固有の戻り値または観測可能な事後条件を推奨してください。
- アサーションの多様性が十分であれば、その旨を述べてください。 テストスイートの多様性が高いと結論づける報告も、まったく問題ありません。
ステップ 6:調査結果を報告する
テストスイートの規模と複雑さに応じて、レポートの詳しさを調整してください。 以下の構成は、大規模なテストスイート(およそ 15 件以上のテスト、または複数ファイルのプロジェクト)向けの完全なテンプレートです。小規模または単純な入力(数件のテストしかない単一ファイル)の場合は、すべてのセクションを出力しないでください。単純な入力に複数セクションのダッシュボードを付けても雑音となり、答えが埋もれてしまいます。代わりに、ユーザーの質問に直接、簡潔に回答してください。つまり、アサーションがないテスト、または実質的なアサーションがないテストがどれか、アサーションの品質に関する総合的な評価、具体的な改善案を示します(意図的なスモークテストと、実際の検証を装っているテストを引き続き区別してください)。入力にとって有用な情報があるセクションだけを使ってください。簡潔な指標の概要に、アサーションのないテストの一覧と推奨事項を添えれば十分な場合も多くあります。評価基準上重要な内容(アサーションのないテスト/実質的なアサーションの特定、品質の評価、具体的な改善案)は省略しないでください。情報を増やさない構成上の冗長さだけを省いてください。
5〜8 件のテストがあるファイルでは、原則として、評価を 1 つと、各テストを簡潔にまとめた表を 1 つ提示してください。呼び出し元から指標を求められていない限り、カテゴリ別の分布ダッシュボードや仮定上の失敗例は省略してください。アサーションの述語と、利用可能な本番コードの挙動によって裏付けられる反例だけを示してください。
次の構成で分析を提示してください。
-
概要ダッシュボード — 主要な指標をまとめた一覧表:
| Metric | Value | Assessment | |-------------------------------|--------|------------| | Total tests | 25 | — | | Average assertions per test | 2.4 | Moderate | | Assertion type spread | 5/12 | Low | | Tests with zero assertions | 3 (12%)| Concerning | | Tests with only trivial asserts | 4 (16%)| Acceptable | | Tests with negative assertions | 2 (8%) | Below target | | Single-category tests | 15 (60%)| High | -
カテゴリ別の内訳 — アサーションの各カテゴリについて、以下を示します。
- そのカテゴリを使用しているテストの数
- コードからの代表的な例
- テスト対象のコードに照らして、使用が過剰か不足しているか
-
ギャップ分析 — 本番コードが利用できる場合は、以下を特定します。
- テストされているものの、等価性チェックしか行われていない動作
- 例外アサーションがないエラーパス
- 状態を変更するメソッドのうち、状態が検証されていないもの
- 内容が確認されていないまま返されるコレクション
-
推奨事項 — 優先順位を付けた改善案:
- 追加のアサーション型を使うことで、特に改善が見込めるテスト
- 不足しているアサーションカテゴリと、それが重要な理由
- 追加できるアサーションの具体例
-
アサーションのないテスト — 該当するテストがある場合は、それぞれのメソッド名と、何をテストしているように見えるかを列挙し、アサーションを追加するか、意図的なスモークテストとして明示するかをユーザーが判断できるようにします。
検証
- テストスイート内のすべてのアサーションが、少なくとも 1 つのカテゴリに分類されている
- 指標が正しく算出されている(合計が一致している)
- 実質的なアサーションがないテストが適切に特定されている(過剰に該当扱いしていない)
- 例外テストがアサーション数の少なさを理由に不当に評価されていない
- 意味のあるプロパティに対するブール値アサーションが、実質的でないものとして分類されていない
- 弱いアサーションに関する主張すべてに、該当するマッチャーが受け入れてしまう現実的な反例が含まれている
- Jest のマッチャーの意味が正確である(
toBeDefinedとundefinedの違い、toMatchObjectにおける部分一致と同一性/完全一致の違い) - 推奨事項が具体的である(特定のテストメソッドを挙げ、具体的なアサーション型を提案している)
- テストスイートに十分な多様性がある場合、レポートでその点を認めている
よくある落とし穴
| 落とし穴 | 解決策 |
|---|---|
| アサーション数が少ないことを理由に例外テストを減点する | 例外アサーションは単独で完結しているため、これらには件数の警告をスキップします |
| 値のチェックより先に行われる null/None/nil チェックを自明なものとして指摘する | null/None/nil チェックだけが唯一のアサーションであるテストのみを指摘します |
| ブール値アサーションをすべて自明なものとして数える | 常に真となるアサーション(Assert.IsTrue(true)、assert True、expect(true).toBe(true))のみが自明です |
| フレームワークごとの違いを無視する | フレームワークごとにアサーション API は異なるため、必ず対応する言語拡張を先に確認します。MSTest の Assert.AreEqual、xUnit の Assert.Equal、NUnit の Is.EqualTo、pytest の裸の assert ==、Jest の expect().toBe()、Go の if … { t.Error… } はすべて等価性カテゴリに分類されます |
| 裸のアサーション形式をフレームワーク不足として扱う | 裸の assert(pytest)、if got != want { t.Error... }(Go)、assert!()(Rust)は標準的な形式です。適切なカテゴリに数えます |
| モック呼び出しの検証をアサーションなしとして扱う | verify(mock).method(...)、expect(mock).toHaveBeenCalledWith(...)、Should -Invoke は状態/副作用アサーションです |
| 多様性のためだけに多様性を勧める | テスト対象のコードに実際のバグがあった場合に検出できるアサーション型のみを追加するよう提案します |
| 暗黙のアサーションを見落とす | 例外アサーションは「例外」と「否定」の両方に該当します。スナップショットテストやプロパティベーステストも、暗黙の構造を持つ実際のアサーションです |
| アサーションが await されていない非同期テスト | TUnit、.resolves/.rejects を使う Jest、pytest-asyncio、Swift Testing、Kotest では、アサーションが await されていないテストは、気付かれないまま成功します。アサーション呼び出しが存在していても、アサーションなしとして扱います |