mcpField
W
writing-mstest-tests

writing-mstest-tests

github.com/dotnetテスト・品質5.8k3.6k 回インストール1 日前に更新
リポジトリ

writing-mstest-tests について

プロジェクトに導入済みのテストスタックに合わせて、MSTest テストの作成や保守を支援するスキルです。バージョンに適したアサーションと既存のリポジトリ規約を重視します。

主な機能

  1. 01導入済み MSTest バージョンの確認
  2. 02互換性のあるアサーション API の選択
  3. 03テスト命名と AAA 構成の適用
  4. 04例外とコレクションの検証
  5. 05リポジトリのテスト規約の尊重

活用シーン

  1. 01既存 MSTest アサーションの改善
  2. 02MSTest 診断の修正
  3. 03バージョン互換のデータ駆動テスト作成
  4. 04プロジェクト規約に沿ったテスト調整

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dotnet/skills --skill writing-mstest-tests

インストール後、Claude Code で/writing-mstest-testsを入力して読み込まれたか確認します。

同梱ファイルreferences/advanced-mstest-patterns.md

詳細

MSTest テストの作成

プロジェクトにインストールされているテストスタックの API レベルや規約を超えずに、効果的な MSTest 単体テストを作成できるよう支援します。

リポジトリのオーバーレイ

リポジトリに関連するすべてのタスクで、読み取り専用のファイル検査が許可されている場合は、ほかの調査を行う前に、リポジトリのルートにある .agents/skill-overlays/dotnet-test/writing-mstest-tests.md を確認してください。編集を伴わないコードやアドバイスの依頼も対象です。「実行しないでください」という指示は、オーバーレイを読むことを禁止するものではありません。ファイルが存在する場合は、作業を始める前に一度読み、リポジトリ固有の命名規則、レイアウト、フレームワーク、ポリシーに関する指定を適用してください。 フロントマターで core: dotnet-test/writing-mstest-tests、binding-revision: "1"、mode: extend が宣言されていることを確認してください。いずれかの値が欠落しているか異なる場合は、不一致を報告し、オーバーレイを無視して、このスキルの汎用的なガイダンスに従ってください。 明示的なユーザー指示と確認済みのプロジェクト制約はオーバーレイより優先されます。オーバーレイは、このスキルの汎用的な既定値や例より優先されます。ファイルが存在するものの読み取れない場合、またはリポジトリと矛盾する場合は、問題を報告し、オーバーレイを無視して、確認済みのプロジェクト制約に従いつつ汎用的なガイダンスを適用してください。ファイルがない場合は、通常どおり続行してください。 タスクがリポジトリに関連しない場合、またはユーザーがすべてのファイルやツールへのアクセスを明示的に禁止した場合に限り、確認を省略してください。オーバーレイによってツールの権限やタスクの範囲を拡大することはできません。

使用する場面

  • 具体的な修正を実施して、既存の MSTest テストを改善または最新化したい場合
  • MSTest のアサーション API、データ駆動型パターン、テストのライフサイクルについて質問がある場合
  • Assert.IsTrue を、より具体的なアサーション(コレクション、null、型、比較)に置き換えたい場合
  • テストでのハードキャストを型チェックアサーションに置き換えたい場合
  • 特定の MSTest テストのバグや、失敗するアサーションの修正を支援してほしい場合
  • Assert.AreEqual の引数の順序(期待値が先、実際の値が後)の入れ替わりを修正したい場合
  • DynamicData を IEnumerable<object[]> から ValueTuple ベースのデータに変換したい場合
  • MSTest アナライザーの診断(MSTESTxxxx の警告やエラー)を修正または理解したい場合

使用しない場面

  • テスト品質の監査、アンチパターンの検出、不安定なテストの調査が必要な場合(test-anti-patterns を使用してください)
  • テストの実行が必要な場合(run-tests スキルを使用してください)
  • MSTest v1/v2 から v3 へのアップグレードが必要な場合(migrate-mstest-v1v2-to-v3 を使用してください)
  • MSTest v3 から v4 へのアップグレードが必要な場合(migrate-mstest-v3-to-v4 を使用してください)
  • CI/CD パイプラインの設定が必要な場合
  • xUnit、NUnit、または TUnit を使用している場合(MSTest ではありません)

入力

入力必須説明
テスト対象のコードいいえテスト対象となる本番コード
既存のテストコードいいえ修正、更新、または最新化する現在のテスト
テストシナリオの説明いいえユーザーがテストしたい動作

回答のガイドライン

  • 特定の API やパターンに関する質問(アサーション、データ駆動、ライフサイクル):該当するワークフローの手順に直接進んでください。ワークフロー全体には従わないでください。
  • テストを一から新規作成する場合:code-testing に引き継いでください。このスキルは、MSTest の API/バージョンに関するガイダンスを補助する目的でのみ使用してください。
  • 既存のテストをレビューして修正する場合:存在する問題のみを修正してください。無関係な改善は追加しないでください。
  • アサーションの変換:修正後の呼び出しを示し、その後、意味上の理由を1文で述べてください。Assert.AreEqual では、expected を先に、actual を後に指定し、これによって失敗時の Expected/Actual ラベルが維持されることを説明してください。
  • 境界値/比較の変換:条件を維持し、期待する境界値を先に、観測値を最後に指定してください。 score > 0 -> Assert.IsGreaterThan(0, score)、 score < 100 -> Assert.IsLessThan(100, score)、 score >= 60 && score <= 90 -> Assert.IsInRange(60, 90, score)。 元の式の左から右への順序に合わせるために、これらの引数の順序を逆にしないでください。
  • 例外の変換:例外をスローする操作はラムダ内で実行し、ThrowsExactly<T>(完全一致する型)と Throws<T>(その型または派生型)を区別してください。また、ParamName などのプロパティが動作の一部である場合は、返された例外を捕捉してください。
  • 提示されたコードに関する依頼:コメントだけのプレースホルダーではなく、代表的なメソッド本体を完全な形で返してください。ライフサイクルや環境ポリシーが依頼に含まれる場合は、実際の操作を維持し、対称となるセットアップ/クリーンアップを示してください。

ワークフロー

ステップ 1:プロジェクトの設定を確認する

テストプロジェクト、packages.config、アセンブリ参照の HintPath の値を確認し、MSTest の正確なバージョンとプロジェクトシステムを特定してください。

  • MSTest.Sdk を使用している場合:プロジェクト SDK 宣言または global.json の msbuild-sdks から正確なバージョンを特定してください。最新の API が利用できると決めつけないでください。
  • MSTest メタパッケージを使用している場合:正確なパッケージバージョンを特定してください。
  • MSTest.TestFramework と MSTest.TestAdapter を使用している場合:機能を利用できるバージョンか確認してください。
  • 従来の非 SDK 形式の XML(ToolsVersion、Microsoft.CSharp.targets、明示的な <Compile Include>)および/または packages.config を使用している場合:そのプロジェクトシステムを維持し、新しいテストファイルをそれぞれ <Compile Include> に追加してください。

また、代表的なテストを調べて、カスタムの基底フィクスチャー、ヘルパーライブラリ、モックの構文、命名規則、セットアップ、データビルダーを確認してください。以下の例よりも、既存の規約とインストール済みバージョンを優先してください。ユーザーが移行を明示的に依頼しない限り、MSTest、Moq、NBuilder、またはプロジェクト形式をアップグレードしないでください。

MSTest API の利用可能性
API/パターン最小バージョン互換性のある代替方法
Assert.ThrowsExactly*、統合された Assert.Contains/HasCount/IsEmpty/IsNotEmpty3.8Assert.ThrowsException*、CollectionAssert、StringAssert
Assert.IsGreaterThan、IsLessThan、IsInRange、StartsWith、EndsWith、MatchesRegex3.10明確なメッセージを付けた Assert.IsTrue、または StringAssert
ジェネリック Assert.IsInstanceOfType<T>(value, out var typed)3.4~3.11 のみ3.0~3.3 では非ジェネリックのアサーション後にキャストし、v4 では型指定された値を直接返します
ValueTuple の DynamicData3.7IEnumerable<object[]>
TestContext のコンストラクターインジェクション3.6インスタンスの TestContext プロパティ
[Retry]、[OSCondition]3.8組み込みの再試行/OS 条件機能はありません。不安定なテストを修正するか、既存の条件メカニズムを維持してください
[CICondition]3.10プロジェクト固有の既存の条件メカニズム

たとえば、MSTest 3.5.x では Assert.ThrowsExactly、 Assert.Contains、ValueTuple の DynamicData、コンストラクターで 注入する TestContext を使用できません。

これを必須条件として扱ってください。バージョンを確認したら、その 最小バージョンを満たしていない限り、このスキル内の新しい例を コピーしないでください。

MSTest.Sdk または MSTest メタパッケージを推奨するのは、実際に新規プロジェクトを作成する場合に限ります。


<Project Sdk="MSTest.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>
</Project>

MSTest.Sdk を使用する場合は、すべてのテストプロジェクトのバージョンをまとめて更新できるよう、プロジェクトファイルではなく global.json にバージョンを指定してください。

{
  "msbuild-sdks": {
    "MSTest.Sdk": "3.8.2"
  }
}

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="MSTest" Version="3.8.2" />
  </ItemGroup>
</Project>
ステップ 2:規約に従ってテストクラスを作成する

スイートで確立されている基底クラスやライフサイクルと矛盾しない範囲でのみ、次の構造上の規約を適用してください。

  • パフォーマンスと設計の明確さのため、テストクラスに sealed を付けます
  • クラスには [TestClass]、テストメソッドには [TestMethod] を使用します
  • Arrange-Act-Assert(AAA)パターンに従います
  • テストには MethodName_Scenario_ExpectedBehavior という命名規則を使用します
  • [ProjectName].Tests という命名規則で、個別のテストプロジェクトを使用します
[TestClass]
public sealed class OrderServiceTests
{
    [TestMethod]
    public void CalculateTotal_WithDiscount_ReturnsReducedPrice()
    {
        // Arrange
        var service = new OrderService();
        var order = new Order { Price = 100m, DiscountPercent = 10 };

        // Act
        var total = service.CalculateTotal(order);

        // Assert
        Assert.AreEqual(90m, total);
    }
}
ステップ 3:バージョン互換性のあるアサーション API を使用する

インストールされている MSTest のバージョンでサポートされている、最も具体的なアサーションを選んでください。 より具体的なアサーションは、失敗時のメッセージを改善し、テストの意図を明確にします。ただし、コンパイルできない「最新の」アサーションよりも、互換性のある StringAssert、CollectionAssert、または Assert.IsTrue の呼び出しを使う方が適切です。

テスト対象アサーション
2 つの値が等しいAssert.AreEqual(expected, actual)
同じオブジェクトインスタンス(参照の同一性)Assert.AreSame(expected, actual)
値が null であるAssert.IsNull(value)
値が null ではないAssert.IsNotNull(value)
コレクションが空であるAssert.IsEmpty(collection)(3.8 以降)、または CollectionAssert/件数アサーション
コレクションが空ではないAssert.IsNotEmpty(collection)(3.8 以降)、または件数アサーション
コレクションに項目がちょうど N 個あるAssert.HasCount(N, collection)(3.8 以降)、または件数に対する Assert.AreEqual
コレクションに項目が含まれるAssert.Contains(item, collection)(3.8 以降)、または CollectionAssert.Contains
コレクションに項目が含まれないAssert.DoesNotContain(item, collection)(3.8 以降)、または CollectionAssert.DoesNotContain
オブジェクトが特定の型であるAssert.IsInstanceOfType<T>(value)
コードが例外をスローするAssert.ThrowsExactly<T>(3.8 以降)、または Assert.ThrowsException<T>(それ以前)

MSTest 3.8 以降では、両方の方法がある場合、StringAssert や CollectionAssert よりも Assert クラスのメソッドを優先してください。古いバージョンでは、互換性のある専用クラスを引き続き使用してください。 独立したコレクションのプロパティが複数指定されている場合、別のアサーションで暗黙的に確認できるとしても、それぞれの意味に沿ったチェックを明示してください。たとえば、要求された診断情報で空/空でない状態を区別する場合は IsNotEmpty を残し、その後に別個の要素数保証として HasCount と ContainsSingle を使用してください。

等価性、null、参照のチェック
Assert.AreEqual(expected, actual);      // Value equality
Assert.AreSame(expected, actual);       // Reference equality -- same object instance
Assert.IsNull(value);
Assert.IsNotNull(value);
例外のテスト
// MSTest 3.8+
var ex = Assert.ThrowsExactly<ArgumentNullException>(() => service.Process(null));
Assert.AreEqual("input", ex.ParamName);

// Async
var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(
    async () => await service.ProcessAsync(null));
  • Assert.Throws<T> は T またはその派生型に一致します
  • Assert.ThrowsExactly<T> は正確に型 T のみに一致します

MSTest 3.7 以前では、互換性のある API を使用します。

var ex = Assert.ThrowsException<ArgumentNullException>(
    () => service.Process(null));
コレクションのアサーション
// MSTest 3.8+
Assert.Contains(expectedItem, collection);
Assert.DoesNotContain(unexpectedItem, collection);
var single = Assert.ContainsSingle(collection);  // Returns the single element
Assert.HasCount(3, collection);
Assert.IsEmpty(collection);
Assert.IsNotEmpty(collection);

以前のバージョンでは、CollectionAssert.Contains、 CollectionAssert.DoesNotContain、および Assert.AreEqual(expectedCount, collection.Count) を使用します。

汎用的な Assert.IsTrue を専用のアサーションに置き換えます。失敗時のメッセージがより明確になります。

代わりに使用するもの使用するもの
Assert.IsTrue(list.Count > 0)Assert.IsNotEmpty(list)
Assert.IsTrue(list.Count == 0)Assert.IsEmpty(list)
Assert.IsTrue(list.Count() == 3)Assert.HasCount(3, list)
Assert.IsTrue(x != null)Assert.IsNotNull(x)
Assert.IsTrue(x == null)Assert.IsNull(x)
同じインスタンスに対する Assert.AreEqual(a, b)Assert.AreSame(a, b) -- 参照の同一性
Assert.IsTrue(!list.Contains(item))Assert.DoesNotContain(item, list)
list.Single(predicate) + Assert.IsNotNullAssert.ContainsSingle(list)
Assert.IsTrue(list.Contains(item))Assert.Contains(item, list)
文字列のアサーション
// MSTest 3.10+
Assert.Contains("expected", actualString);
Assert.StartsWith("prefix", actualString);
Assert.EndsWith("suffix", actualString);
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);

以前のバージョンでは、StringAssert.Contains、StringAssert.StartsWith、 StringAssert.EndsWith、および StringAssert.Matches を使用します。

型のアサーション

MSTest 3.x は単一の API レベルではありません。インストールされている マイナーバージョンでサポートされる形式を選択します。

// MSTest 3.0-3.3
Assert.IsInstanceOfType(result, typeof(MyHandler));
var typed = (MyHandler)result; // Safe because the assertion stops a mismatch.
// MSTest 3.4-3.11 -- out parameter
Assert.IsInstanceOfType<MyHandler>(result, out var typed);
typed.Handle();
// MSTest 4.x -- returns the proven value directly
var typed = Assert.IsInstanceOfType<MyHandler>(result);
比較のアサーション
Assert.IsGreaterThan(lowerBound, actual);
Assert.IsLessThan(upperBound, actual);
Assert.IsInRange(low, high, actual);
ステップ 4:複数の入力にはデータ駆動型テストを使用する
インライン値には DataRow
[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0, DisplayName = "Zeros")]
[DataRow(-1, 1, 0)]
public void Add_ReturnsExpectedSum(int a, int b, int expected)
{
    Assert.AreEqual(expected, Calculator.Add(a, b));
}
ValueTuple を使用した DynamicData(複雑なデータに推奨)

MSTest 3.7 以降では、型安全性のために IEnumerable<object[]> よりも ValueTuple の戻り値の型を優先します。古いバージョンでは IEnumerable<object[]> を使用します。

タプル要素名を使うと、どの位置がどのテストパラメーターに対応するかを記述でき、 タプル要素の型によって互換性のない値をコンパイル時に検出できます。ただし、 DynamicData が位置に依存しなくなるわけではなく、同じ型の要素を入れ替えても コンパイルが通ることがあります。そうではないと主張しないでください。型付きの 位置データだけでなく、行にカスタム表示名やメタデータが必要な場合は、MSTest 3.8 以降で TestDataRow<T> を使用します。

[TestMethod]
[DynamicData(nameof(DiscountTestData))]
public void ApplyDiscount_ReturnsExpectedPrice(decimal price, int percent, decimal expected)
{
    var result = PriceCalculator.ApplyDiscount(price, percent);
    Assert.AreEqual(expected, result);
}

// ValueTuple -- preferred (MSTest 3.7+)
public static IEnumerable<(decimal price, int percent, decimal expected)> DiscountTestData =>
[
    (100m, 10, 90m),
    (200m, 25, 150m),
    (50m, 0, 50m),
];

MSTest 3.8 以降でテストケースごとのメタデータが必要な場合は、 TestDataRow<T> を使用します。

public static IEnumerable<TestDataRow<(decimal price, int percent, decimal expected)>> DiscountTestDataWithMetadata =>
[
    new((100m, 10, 90m)) { DisplayName = "10% discount" },
    new((200m, 25, 150m)) { DisplayName = "25% discount" },
    new((50m, 0, 50m)) { DisplayName = "No discount" },
];
ステップ 5:テストのライフサイクルを適切に扱う
  • 既存のテストスイートが対応している場合はコンストラクターでの初期化を優先します。フィクスチャのアーキテクチャを不用意に書き換えるのではなく、共有の FixtureBase<TSut> または既存の [TestInitialize] ライフサイクルを維持します。
  • 非同期の初期化に限り [TestInitialize] を使用し、同期部分にはコンストラクターを併用します
  • 失敗時にも実行する必要があるクリーンアップには [TestCleanup] を使用します
  • TestContext のコンストラクター経由での注入は MSTest 3.6 以降でのみ使用します。それ以前ではインスタンスプロパティを使用します。
[TestClass]
public sealed class RepositoryTests
{
    private readonly TestContext _testContext;
    private readonly FakeDatabase _db;  // readonly -- guaranteed by constructor

    public RepositoryTests(TestContext testContext)
    {
        _testContext = testContext;
        _db = new FakeDatabase();  // sync init in ctor
    }

    [TestInitialize]
    public async Task InitAsync()
    {
        // Use TestInitialize ONLY for async setup
        await _db.SeedAsync();
    }

    [TestCleanup]
    public void Cleanup() => _db.Reset();
}
実行順序
  1. [AssemblyInitialize] -- アセンブリごとに 1 回
  2. [ClassInitialize] -- クラスごとに 1 回
  3. テストごと:
    • TestContext プロパティの注入を使用する場合:コンストラクター -> TestContext プロパティを設定 -> [TestInitialize]
    • TestContext のコンストラクター注入を使用する場合:コンストラクター(TestContext を受け取る) -> [TestInitialize]
  4. テストメソッド
  5. [TestCleanup] -> DisposeAsync -> Dispose -- テストごと
  6. [ClassCleanup] -- クラスごとに 1 回
  7. [AssemblyCleanup] -- アセンブリごとに 1 回
ステップ 6:キャンセルとタイムアウトのパターンを適用する

インストールされている MSTest バージョンがトークンを直接公開する場合(3.11 以降)は、[Timeout(milliseconds, CooperativeCancellation = true)] とともに TestContext.CancellationToken を使用します。MSTest 3.6.4~3.10 では、代わりに 協調キャンセルを有効にして TestContext.CancellationTokenSource.Token を使用します。 通常の [Timeout] だけでは、フレームワークのトークンによって実行中の処理が停止する ことは保証されません。より古いバージョンでは、キャンセル自体をテストする場合に、 テスト側で管理する CancellationTokenSource を使用します。

// MSTest 3.11+
[TestMethod]
[Timeout(5000, CooperativeCancellation = true)]
public async Task FetchData_ReturnsWithinTimeout()
{
    var result = await _client.GetDataAsync(_testContext.CancellationToken);
    Assert.IsNotNull(result);
}
ステップ 7: 必要に応じて高度な機能を使用する

リトライ、条件付き実行、または並列化の要求については、 高度な MSTest パターンを参照してください。そこに記載されている バージョンゲートを適用し、実際にテストされる操作、アサーション、クリーンアップを維持してください。

ステップ 8: MSTest アナライザーの診断を修正する (MSTESTxxxx)

MSTESTxxxx の要求については、 高度な MSTest パターンを参照し、その規則に対応する バージョン互換の修正を適用してから、診断が解消されたことを確認するためにリビルドしてください。規則を抑制するよりも、コードを修正することを優先してください。

ステップ 9: ファイルに基づく修正を検証する

ユーザーがリポジトリの編集を求め、実行を禁止していない場合は、編集後に影響を受ける範囲で最も限定的な dotnet test コマンドを実行してください。検出されたテスト数が 0 のままプロセスが成功しても、検証とは言えません。意図したテストケースが検出され、合格することを必須としてください。

コンパイルによって、修正済みの既存スイートの実行を妨げる直接的な結合のあるソースの問題(たとえば、提供された運用ファイルにおける名前空間インポートの欠落)が明らかになった場合は、その最小限の修正のみを行い、再実行してください。パッケージをアップグレードしたり、モダナイゼーションの範囲を広げたりしないでください。実際のテスト数と行った修正を報告してください。実行していないテストや出力のないテストを合格として提示してはなりません。 最終的な引き継ぎでは、要求されたすべてのモダナイゼーションを、正確に修正された構文要素に対応付け、合格したテストコマンドを引用してください。期待値/実測値の順序、正確な型チェック、データ検出、またはクラスの形状が明示的な要件であった場合には、一般的な「モダナイズ済み」という要約に頼らないでください。

コメント