mcpField
C
csharp-mstest

csharp-mstest

github.com/githubテスト・品質40.5k8.9k 回インストール今日更新
リポジトリ

csharp-mstest について

C# 開発者向けに、MSTest 3.x/4.x を使ったユニットテストの実践方法をまとめたスキルです。テスト構成、アサーション、ライフサイクル、データ駆動テスト、実行方法を扱います。

主な機能

  1. 01MSTest のクラスとメソッドの規約
  2. 02Arrange-Act-Assert による構成
  3. 03最新のアサーションと例外 API
  4. 04ライフサイクルと TestContext の指針
  5. 05データ駆動・再試行・並列テスト

活用シーン

  1. 01独立した MSTest テストプロジェクトの設定
  2. 02同期・非同期コードのテスト作成
  3. 03例外やコレクション結果の検証
  4. 04CI とトレーサビリティ向けのテスト整理

インストール

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

$npx skills add https://github.com/github/awesome-copilot --skill csharp-mstest

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

詳細

MSTest のベストプラクティス (MSTest 3.x/4.x)

あなたの目標は、現代的な MSTest を使用し、最新の API とベストプラクティスに従って、効果的な単体テストを書く手助けをすることです。

プロジェクトのセットアップ

  • [ProjectName].Tests という命名規則で別のテストプロジェクトを使用する
  • MSTest 3.x 以降の NuGet パッケージを参照する (アナライザーを含む)
  • プロジェクトのセットアップを簡素化するために MSTest.Sdk の使用を検討する
  • dotnet test でテストを実行する

テストクラスの構造

  • テストクラスには [TestClass] 属性を使用する
  • パフォーマンスと設計の明確さのために、テストクラスはデフォルトで sealed にする
  • テストメソッドには [TestMethod] を使用する ([DataTestMethod] より優先)
  • Arrange-Act-Assert (AAA) パターンに従う
  • テストの名前には MethodName_Scenario_ExpectedBehavior というパターンを使用する
[TestClass]
public sealed class CalculatorTests
{
    [TestMethod]
    public void Add_TwoPositiveNumbers_ReturnsSum()
    {
        // Arrange
        var calculator = new Calculator();

        // Act
        var result = calculator.Add(2, 3);

        // Assert
        Assert.AreEqual(5, result);
    }
}

テストのライフサイクル

  • [TestInitialize] よりもコンストラクターを優先する - readonly フィールドが使用でき、標準的な C# のパターンに従えます
  • テストが失敗した場合でも必ず実行する必要があるクリーンアップには [TestCleanup] を使用する
  • 非同期のセットアップが必要な場合は、コンストラクターと非同期の [TestInitialize] を組み合わせる
[TestClass]
public sealed class ServiceTests
{
    private readonly MyService _service;  // readonly enabled by constructor

    public ServiceTests()
    {
        _service = new MyService();
    }

    [TestInitialize]
    public async Task InitAsync()
    {
        // Use for async initialization only
        await _service.WarmupAsync();
    }

    [TestCleanup]
    public void Cleanup() => _service.Reset();
}
実行順序
  1. アセンブリの初期化 - [AssemblyInitialize] (テストアセンブリごとに 1 回)
  2. クラスの初期化 - [ClassInitialize] (テストクラスごとに 1 回)
  3. テストの初期化 (すべてのテストメソッドに対して):
    1. コンストラクター
    2. TestContext プロパティの設定
    3. [TestInitialize]
  4. テストの実行 - テストメソッドが実行される
  5. テストのクリーンアップ (すべてのテストメソッドに対して):
    1. [TestCleanup]
    2. DisposeAsync (実装されている場合)
    3. Dispose (実装されている場合)
  6. クラスのクリーンアップ - [ClassCleanup] (テストクラスごとに 1 回)
  7. アセンブリのクリーンアップ - [AssemblyCleanup] (テストアセンブリごとに 1 回)

モダンなアサーション API

MSTest は Assert、StringAssert、CollectionAssert という 3 つのアサーションクラスを提供しています。

Assert クラス - コアとなるアサーション
// Equality
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(notExpected, actual);
Assert.AreSame(expectedObject, actualObject);      // Reference equality
Assert.AreNotSame(notExpectedObject, actualObject);

// Null checks
Assert.IsNull(value);
Assert.IsNotNull(value);

// Boolean
Assert.IsTrue(condition);
Assert.IsFalse(condition);

// Fail/Inconclusive
Assert.Fail("Test failed due to...");
Assert.Inconclusive("Test cannot be completed because...");
例外のテスト ([ExpectedException] より優先)
// Assert.Throws - matches TException or derived types
var ex = Assert.Throws<ArgumentException>(() => Method(null));
Assert.AreEqual("Value cannot be null.", ex.Message);

// Assert.ThrowsExactly - matches exact type only
var ex = Assert.ThrowsExactly<InvalidOperationException>(() => Method());

// Async versions
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetAsync(url));
var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () => await Method());
コレクションのアサーション (Assert クラス)
Assert.Contains(expectedItem, collection);
Assert.DoesNotContain(unexpectedItem, collection);
Assert.ContainsSingle(collection);  // exactly one element
Assert.HasCount(5, collection);
Assert.IsEmpty(collection);
Assert.IsNotEmpty(collection);
文字列のアサーション (Assert クラス)
Assert.Contains("expected", actualString);
Assert.StartsWith("prefix", actualString);
Assert.EndsWith("suffix", actualString);
Assert.DoesNotStartWith("prefix", actualString);
Assert.DoesNotEndWith("suffix", actualString);
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);
Assert.DoesNotMatchRegex(@"\d+", textOnly);
比較のアサーション
Assert.IsGreaterThan(lowerBound, actual);
Assert.IsGreaterThanOrEqualTo(lowerBound, actual);
Assert.IsLessThan(upperBound, actual);
Assert.IsLessThanOrEqualTo(upperBound, actual);
Assert.IsInRange(actual, low, high);
Assert.IsPositive(number);
Assert.IsNegative(number);
型のアサーション
// MSTest 3.x - uses out parameter
Assert.IsInstanceOfType<MyClass>(obj, out var typed);
typed.DoSomething();

// MSTest 4.x - returns typed result directly
var typed = Assert.IsInstanceOfType<MyClass>(obj);
typed.DoSomething();

Assert.IsNotInstanceOfType<WrongType>(obj);
Assert.That (MSTest 4.0 以降)
Assert.That(result.Count > 0);  // Auto-captures expression in failure message
StringAssert クラス

Note: 利用可能な場合は Assert クラスの同等のもの (例: StringAssert.Contains(actual, "expected") ではなく Assert.Contains("expected", actual)) を優先してください。

StringAssert.Contains(actualString, "expected");
StringAssert.StartsWith(actualString, "prefix");
StringAssert.EndsWith(actualString, "suffix");
StringAssert.Matches(actualString, new Regex(@"\d{3}-\d{4}"));
StringAssert.DoesNotMatch(actualString, new Regex(@"\d+"));
CollectionAssert クラス

Note: 利用可能な場合は Assert クラスの同等のもの (例: Assert.Contains) を優先してください。

// Containment
CollectionAssert.Contains(collection, expectedItem);
CollectionAssert.DoesNotContain(collection, unexpectedItem);

// Equality (same elements, same order)
CollectionAssert.AreEqual(expectedCollection, actualCollection);
CollectionAssert.AreNotEqual(unexpectedCollection, actualCollection);

// Equivalence (same elements, any order)
CollectionAssert.AreEquivalent(expectedCollection, actualCollection);
CollectionAssert.AreNotEquivalent(unexpectedCollection, actualCollection);

// Subset checks
CollectionAssert.IsSubsetOf(subset, superset);
CollectionAssert.IsNotSubsetOf(notSubset, collection);

// Element validation
CollectionAssert.AllItemsAreInstancesOfType(collection, typeof(MyClass));
CollectionAssert.AllItemsAreNotNull(collection);
CollectionAssert.AllItemsAreUnique(collection);

データ駆動型テスト

DataRow
[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0, DisplayName = "Zeros")]
[DataRow(-1, 1, 0, IgnoreMessage = "Known issue #123")]  // MSTest 3.8+
public void Add_ReturnsSum(int a, int b, int expected)
{
    Assert.AreEqual(expected, Calculator.Add(a, b));
}
DynamicData

データソースは次のいずれかの型を返すことができます:

  • IEnumerable<(T1, T2, ...)> (ValueTuple) - 推奨、型の安全性を提供します (MSTest 3.7 以降)
  • IEnumerable<Tuple<T1, T2, ...>> - 型の安全性を提供します
  • IEnumerable<TestDataRow> - 型の安全性に加えて、テストのメタデータ (表示名、カテゴリ) を制御できます
  • IEnumerable<object[]> - 最も非推奨、型の安全性がありません

Note: 新しいテストデータメソッドを作成する場合は、IEnumerable<object[]> よりも ValueTuple または TestDataRow を優先してください。object[] のアプローチはコンパイル時の型チェックを提供せず、型の不一致による実行時エラーにつながる可能性があります。

[TestMethod]
[DynamicData(nameof(TestData))]
public void DynamicTest(int a, int b, int expected)
{
    Assert.AreEqual(expected, Calculator.Add(a, b));
}

// ValueTuple - preferred (MSTest 3.7+)
public static IEnumerable<(int a, int b, int expected)> TestData =>
[
    (1, 2, 3),
    (0, 0, 0),
];

// TestDataRow - when you need custom display names or metadata
public static IEnumerable<TestDataRow<(int a, int b, int expected)>> TestDataWithMetadata =>
[
    new((1, 2, 3)) { DisplayName = "Positive numbers" },
    new((0, 0, 0)) { DisplayName = "Zeros" },
    new((-1, 1, 0)) { DisplayName = "Mixed signs", IgnoreMessage = "Known issue #123" },
];

// IEnumerable<object[]> - avoid for new code (no type safety)
public static IEnumerable<object[]> LegacyTestData =>
[
    [1, 2, 3],
    [0, 0, 0],
];

TestContext

TestContext クラスは、テスト実行の情報、キャンセルのサポート、および出力メソッドを提供します。 完全なリファレンスについては TestContext のドキュメント を参照してください。

TestContext へのアクセス
// Property (MSTest suppresses CS8618 - don't use nullable or = null!)
public TestContext TestContext { get; set; }

// Constructor injection (MSTest 3.6+) - preferred for immutability
[TestClass]
public sealed class MyTests
{
    private readonly TestContext _testContext;

    public MyTests(TestContext testContext)
    {
        _testContext = testContext;
    }
}

// Static methods receive it as parameter
[ClassInitialize]
public static void ClassInit(TestContext context) { }

// Optional for cleanup methods (MSTest 3.6+)
[ClassCleanup]
public static void ClassCleanup(TestContext context) { }

[AssemblyCleanup]
public static void AssemblyCleanup(TestContext context) { }
キャンセルトークン

[Timeout] との協調的なキャンセルには、常に TestContext.CancellationToken を使用してください:

[TestMethod]
[Timeout(5000)]
public async Task LongRunningTest()
{
    await _httpClient.GetAsync(url, TestContext.CancellationToken);
}
テスト実行のプロパティ
TestContext.TestName              // Current test method name
TestContext.TestDisplayName       // Display name (3.7+)
TestContext.CurrentTestOutcome    // Pass/Fail/InProgress
TestContext.TestData              // Parameterized test data (3.7+, in TestInitialize/Cleanup)
TestContext.TestException         // Exception if test failed (3.7+, in TestCleanup)
TestContext.DeploymentDirectory   // Directory with deployment items
出力と結果ファイル
// Write to test output (useful for debugging)
TestContext.WriteLine("Processing item {0}", itemId);

// Attach files to test results (logs, screenshots)
TestContext.AddResultFile(screenshotPath);

// Store/retrieve data across test methods
TestContext.Properties["SharedKey"] = computedValue;

高度な機能

不安定なテストのための再試行 (MSTest 3.9 以降)
[TestMethod]
[Retry(3)]
public void FlakyTest() { }
条件付き実行 (MSTest 3.10 以降)

OS や CI 環境に基づいてテストをスキップまたは実行します:

// OS-specific tests
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void WindowsOnlyTest() { }

[TestMethod]
[OSCondition(OperatingSystems.Linux | OperatingSystems.MacOS)]
public void UnixOnlyTest() { }

[TestMethod]
[OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)]
public void SkipOnWindowsTest() { }

// CI environment tests
[TestMethod]
[CICondition]  // Runs only in CI (default: ConditionMode.Include)
public void CIOnlyTest() { }

[TestMethod]
[CICondition(ConditionMode.Exclude)]  // Skips in CI, runs locally
public void LocalOnlyTest() { }
並列化
// Assembly level
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]

// Disable for specific class
[TestClass]
[DoNotParallelize]
public sealed class SequentialTests { }
作業項目のトレーサビリティ (MSTest 3.8+)

テストレポートでのトレーサビリティのために、テストを作業項目にリンクします:

// Azure DevOps work items
[TestMethod]
[WorkItem(12345)]  // Links to work item #12345
public void Feature_Scenario_ExpectedBehavior() { }

// Multiple work items
[TestMethod]
[WorkItem(12345)]
[WorkItem(67890)]
public void Feature_CoversMultipleRequirements() { }

// GitHub issues (MSTest 3.8+)
[TestMethod]
[GitHubWorkItem("https://github.com/owner/repo/issues/42")]
public void BugFix_Issue42_IsResolved() { }

作業項目の関連付けはテスト結果に表示され、次の用途に使用できます:

  • テストカバレッジを要件にトレースする
  • バグ修正を回帰テストにリンクする
  • CI/CD パイプラインでトレーサビリティレポートを生成する

避けるべきよくある間違い

// ❌ Wrong argument order
Assert.AreEqual(actual, expected);
// ✅ Correct
Assert.AreEqual(expected, actual);

// ❌ Using ExpectedException (obsolete)
[ExpectedException(typeof(ArgumentException))]
// ✅ Use Assert.Throws
Assert.Throws<ArgumentException>(() => Method());

// ❌ Using LINQ Single() - unclear exception
var item = items.Single();
// ✅ Use ContainsSingle - better failure message
var item = Assert.ContainsSingle(items);

// ❌ Hard cast - unclear exception
var handler = (MyHandler)result;
// ✅ Type assertion - shows actual type on failure
var handler = Assert.IsInstanceOfType<MyHandler>(result);

// ❌ Ignoring cancellation token
await client.GetAsync(url, CancellationToken.None);
// ✅ Flow test cancellation
await client.GetAsync(url, TestContext.CancellationToken);

// ❌ Making TestContext nullable - leads to unnecessary null checks
public TestContext? TestContext { get; set; }
// ❌ Using null! - MSTest already suppresses CS8618 for this property
public TestContext TestContext { get; set; } = null!;
// ✅ Declare without nullable or initializer - MSTest handles the warning
public TestContext TestContext { get; set; }

テストの構成

  • 機能またはコンポーネントごとにテストをグループ化する
  • フィルタリングには [TestCategory("Category")] を使用する
  • カスタムメタデータには [TestProperty("Name", "Value")] を使用する (例: [TestProperty("Bug", "12345")])
  • 重要なテストには [Priority(1)] を使用する
  • 関連する MSTest アナライザーを有効にする (コンストラクターの優先には MSTEST0020)

モックと分離

  • 依存関係のモックには Moq または NSubstitute を使用する
  • モックを容易にするためにインターフェイスを使用する
  • テスト対象のユニットを分離するために依存関係をモックする

コメント