変異テスト

変異テストは、単体テストの品質を評価する方法です。 変異テストの場合、 Stryker.NET ツールはコード内で自動的に変更を実行し、テストを実行し、結果を含む詳細なレポートを生成します。

テスト シナリオの例

割引を考慮 して価格を 計算する Calculate メソッドを持つサンプル PriceCalculator.cs クラスについて考えてみましょう。

public class PriceCalculator
{
    public decimal CalculatePrice(decimal price, decimal discountPercent)
    {
        if (price <= 0)
        {
            throw new ArgumentException("Price must be greater than zero.");
        }

        if (discountPercent < 0 || discountPercent > 100)
        {
            throw new ArgumentException("Discount percent must be between 0 and 100.");
        }

        var discount = price * (discountPercent / 100);
        var discountedPrice = price - discount;

        return Math.Round(discountedPrice, 2);
    }
}

上記のメソッドは、次の単体テストでカバーされています。

[Fact]
public void ApplyDiscountCorrectly()
{
    decimal price = 100;
    decimal discountPercent = 10;

    var calculator = new PriceCalculator();

    var result = calculator.CalculatePrice(price, discountPercent);

    Assert.Equal(90.00m, result);
}

[Fact]
public void InvalidDiscountPercent_ShouldThrowException()
{
    var calculator = new PriceCalculator();

    Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, -1));
    Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, 101));
}

[Fact]
public void InvalidPrice_ShouldThrowException()
{
    var calculator = new PriceCalculator();

    Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(-10, 10));
}

上記のコードでは、2 つのプロジェクトが強調表示されています。1 つは PriceCalculator として機能するサービス用で、もう 1 つはテスト プロジェクトです。

グローバル ツールをインストールする

まず、 Stryker.NET をインストールします。 これを行うには、次のコマンドを実行する必要があります。

dotnet tool install -g dotnet-stryker

strykerを実行するには、単体テスト プロジェクトが配置されているディレクトリ内のコマンド ラインから呼び出します。

dotnet stryker

テストの実行後、コンソールにレポートが表示されます。

Stryker コンソール レポート

Stryker.NET 、StrykerOutput ディレクトリに詳細な HTML レポートを保存します。

Stryker の最初のレポート

さて、変異型とは何か、そして「生き残った」と「殺された」とは何を意味するのかを考えてみましょう。 ミュータントは、Stryker が意図的に行うコードの小さな変更です。 この考え方は単純です。テストが適切であれば、変更を検出し、失敗するはずです。 それでも合格した場合、テストの強度が十分でない可能性があります。

この例では、ミュータントは式 price <= 0price < 0 などに置き換えたものとなり、その後にユニット テストが実行されます。

Stryker では、いくつかの種類の変異がサポートされています。

タイプ 説明
同等物 同等の演算子は、演算子を同等の演算子に置き換えるために使用されます。 たとえば、x < yx <= yになります。
算術 算術演算子は、算術演算子を同等のものに置き換えるために使用されます。 たとえば、x + yx - yになります。
文字列演算子は、文字列を同等のものに置き換えるために使用されます。 たとえば、"text"""になります。
論理 論理演算子は、論理演算子を同等のものに置き換えるために使用されます。 たとえば、x && yx \|\| yになります。

その他の変異の種類については、「 Stryker.NET: 変異」 のドキュメントを参照してください。

変異テストの結果を解釈する

Stryker.NET を実行すると、ミュータントが除去生存、またはタイムアウトとして分類されるレポートが表示されます。 これらの結果を解釈して処理する方法を次に示します。

  • 強制終了: テストが正常にキャッチされた変更です。 殺された変異型の数が多い場合は、テスト スイートがロジック エラーを効果的に検出することを示します。
  • 見逃された: これらの変更はテストで検出されませんでした。 これらを見直して、テストカバレッジや十分でないアサーションのギャップを特定します。 ミュータントが実際の場合に失敗するターゲット単体テストの追加に焦点を当てます。
  • タイムアウト: これらの変更により、コードがハングしたり、許容時間を超えたりしました。 これは、無限ループまたは最適化されていないパスで発生する可能性があります。 コード ロジックを調査するか、必要に応じてタイムアウトしきい値を増やします。

100% の変異スコアを追いかけないでください。 代わりに、検出されないバグが最もコストがかかるリスクの高い領域やビジネス クリティカルな領域に焦点を当てます。

CI/CD ワークフローへの変異テストの追加

変異テストを継続的インテグレーションと配信ワークフローにシームレスに統合できます。 たとえば、Stryker.NET は、Azure Pipelines または GitHub Actions のセットアップ内で実行するように構成できます。これにより、自動テスト プロセスの一部として品質しきい値を適用できます。

カスタマイズ

パイプラインのしきい値を設定するだけでなく、Stryker.NET はプロジェクトのニーズごとに異なる構成を持つ可能性を提供します。 stryker-config.json ファイルを使用して動作をカスタマイズできます。

{
  "stryker-config": {
    "ignore-mutations": [
      "string",
      "logical"
    ],
    "ignore-methods": [
      "*Logs"
    ],
    "mutate": [
      "!**/Migrations/*",
      "!**/*.Designer.cs"
    ]
  }
}
  • ignore-mutations: テストから除外する変異の種類は、ノイズが多いか、アプリケーション ロジックに関連していないためです。 レポートに Ignoredとして表示されます。
  • ignore-methods: これを使用して、シグネチャに基づいてメソッド全体をスキップできます。 これらは、 Ignoredとしてレポートにも表示されます。 前の例では、"Logs" で終わるすべてのメソッドは無視されます。
  • mutate: このオプションがないと、Stryker はプロジェクト内のすべてのファイルを変更しようとします。 これにより、ファイルまたはフォルダー全体を無視できます。 前の例では、 Migrations フォルダー内のすべてのものとすべて .Designer.cs ファイル (通常は自動生成されます) は無視されます。

詳細については、「 Stryker: 構成」を参照してください。

段階的な改善

コードを変更した後、単体テストが正常に成功した場合、それらは十分に堅牢ではなく、ミュータントは存続します。 変異試験の後、5つの変異株が生き残る。

境界値のテスト データを追加し、もう一度変異テストを実行してみましょう。

[Fact]
public void InvalidPrice_ShouldThrowException()
{
    var calculator = new PriceCalculator();

    // changed price from -10 to 0
    Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(0, 10));
}

[Fact] // Added test for 0 and 100 discount
public void NoExceptionForZeroAnd100Discount()
{
    var calculator = new PriceCalculator();

    var exceptionWhen0 = Record.Exception(() => calculator.CalculatePrice(100, 0));
    var exceptionWhen100 = Record.Exception(() => calculator.CalculatePrice(100, 100));

    Assert.Null(exceptionWhen0);
    Assert.Null(exceptionWhen100);
}

Stryker 2 番目のレポート

ご覧のように、同等のミュータントを修正した後は、文字列の変異のみが残っています。例外メッセージのテキストをチェックすることで簡単に "強制終了" できます。

[Fact]
public void InvalidDiscountPercent_ShouldThrowExceptionWithCorrectMessage()
{
    var calculator = new PriceCalculator();

    var ex1 = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, -1));
    Assert.Equal("Discount percent must be between 0 and 100.", ex1.Message);

    var ex2 = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, 101));
    Assert.Equal("Discount percent must be between 0 and 100.", ex2.Message);
}

[Fact]
public void InvalidPrice_ShouldThrowExceptionWithCorrectMessage()
{
    var calculator = new PriceCalculator();

    var ex = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(0, 10));
    Assert.Equal("Price must be greater than zero.", ex.Message);
}

Stryker の最終レポート

変異テストは、より信頼性の高いテストを改善する機会を見つけるのに役立ちます。 これにより、"幸せなパス" だけでなく、複雑な境界ケースもチェックされ、運用環境でのバグの可能性が低下します。