mcpField
D
dotnet-xunit

dotnet-xunit

github.com/wshaddixテスト・品質86614 回インストール35 週間前に更新
リポジトリ

dotnet-xunit について

dotnet-xunitは、.NET開発者向けのxUnitテストフレームワークのスキルです。xUnit v3を中心に、v2との互換性にも触れながら、テストケース、フィクスチャ、並列実行、アサーション、アナライザーを解説します。

主な機能

  1. 01`[Fact]`と`[Theory]`によるテスト作成
  2. 02クラスおよびコレクションのフィクスチャ利用
  3. 03テストの並列実行設定
  4. 04非同期の初期化と後処理
  5. 05アサーションの拡張とアナライザー活用

活用シーン

  1. 01.NETコードのパラメーター化テスト作成
  2. 02テスト間で高コストのリソースを共有
  3. 03xUnit v2のテストをv3へ移行

インストール

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

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-xunit

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

詳細

dotnet-xunit

.NET 向け xUnit v3 テストフレームワークの機能です。[Fact] と [Theory] 属性、テストフィクスチャ(IClassFixture、ICollectionFixture)、並列実行の設定、非同期セットアップ/ティアダウンのための IAsyncLifetime、カスタムアサーション、xUnit アナライザーを扱います。動作が異なる場合は、v2 との互換性に関する注記も含まれます。

バージョンの前提: xUnit v3 を主な対象とし、ベースラインは .NET 8.0 以降です。v3 の動作が v2 と異なる箇所には、互換性に関する注記を記載しています。xUnit v2 も広く使われているため、移行時に両方のバージョンに遭遇するプロジェクトは少なくありません。

対象外: テストプロジェクトのひな形作成(xUnit プロジェクト、パッケージ参照、Directory.Build.props の作成)は [skill:dotnet-add-testing] が担当します。テスト戦略とテスト種別の判断については、[skill:dotnet-testing-strategy] で扱います。統合テストのパターン(WebApplicationFactory、Testcontainers)については、[skill:dotnet-integration-testing] で扱います。

前提条件: [skill:dotnet-add-testing] を使用してテストプロジェクトのひな形が作成済みで、xUnit パッケージが参照されている必要があります。xUnit v3 のサポートに必要な .NET 8.0 以降のベースラインを確認するには、[skill:dotnet-version-detection] を実行してください。

相互参照:[skill:dotnet-testing-strategy] は何をどのようにテストするかの判断に、[skill:dotnet-integration-testing] は xUnit と WebApplicationFactory、Testcontainers の組み合わせに関する情報を提供します。


xUnit v3 と v2:主な変更点

機能xUnit v2xUnit v3
パッケージxunit (2.x)xunit.v3
ランナーxunit.runner.visualstudioxunit.runner.visualstudio (3.x)
非同期ライフサイクルIAsyncLifetimeIAsyncLifetime(戻り値が ValueTask に変更)
Assert パッケージ同梱独立した xunit.v3.assert(拡張性を求める場合は xunit.v3.assert.source)
並列実行の既定値コレクション単位コレクション単位(同じですが、アセンブリ単位で設定可能)
タイムアウト[Fact] と [Theory] の Timeout プロパティ[Fact] と [Theory] の Timeout プロパティ(変更なし)
テスト出力ITestOutputHelperITestOutputHelper(変更なし)
[ClassData]IEnumerable<object[]> を返すIEnumerable<TheoryDataRow<T>> を返す(型安全)
[MemberData]IEnumerable<object[]> を返すTheoryData<T> と TheoryDataRow<T> をサポート
アサーションメッセージAssert メソッドの省略可能な文字列パラメーターカスタムアサーションを優先して削除(v3.0)。明示的なメッセージには Assert.Fail() を使用

v2 との互換性に関する注記: v2 から移行する場合は、xunit パッケージを xunit.v3 に置き換えてください。ほとんどの [Fact] と [Theory] のテストは変更なしで動作します。主な移行作業は、IAsyncLifetime(戻り値の型が ValueTask に変更)、[ClassData](型安全な行形式)、および削除されたアサーションメッセージのパラメーターへの対応です。


Fact と Theory

[Fact] — 単一のテストケース

パラメーターのないテストには [Fact] を使用します。

public class DiscountCalculatorTests
{
    [Fact]
    public void Apply_NegativePercentage_ThrowsArgumentOutOfRangeException()
    {
        var calculator = new DiscountCalculator();

        var ex = Assert.Throws<ArgumentOutOfRangeException>(
            () => calculator.Apply(100m, percentage: -5));

        Assert.Equal("percentage", ex.ParamName);
    }

    [Fact]
    public async Task ApplyAsync_ValidDiscount_ReturnsDiscountedPrice()
    {
        var calculator = new DiscountCalculator();

        var result = await calculator.ApplyAsync(100m, percentage: 15);

        Assert.Equal(85m, result);
    }
}
[Theory] — パラメーター化されたテスト

同じテストロジックを異なる入力で実行するには、[Theory] を使用します。

[InlineData]

単純な値型に最適です。

[Theory]
[InlineData(100, 10, 90)]      // 10% off 100 = 90
[InlineData(200, 25, 150)]     // 25% off 200 = 150
[InlineData(50, 0, 50)]        // 0% off = no change
[InlineData(100, 100, 0)]      // 100% off = 0
public void Apply_VariousInputs_ReturnsExpectedPrice(
    decimal price, decimal percentage, decimal expected)
{
    var calculator = new DiscountCalculator();

    var result = calculator.Apply(price, percentage);

    Assert.Equal(expected, result);
}
TheoryData<T> を使った [MemberData]

複雑なデータや共有データセットに最適です。

public class OrderValidatorTests
{
    public static TheoryData<Order, bool> ValidationCases => new()
    {
        { new Order { Items = [new("SKU-1", 1)], CustomerId = "C1" }, true },
        { new Order { Items = [], CustomerId = "C1" }, false },              // no items
        { new Order { Items = [new("SKU-1", 1)], CustomerId = "" }, false }, // no customer
    };

    [Theory]
    [MemberData(nameof(ValidationCases))]
    public void IsValid_VariousOrders_ReturnsExpected(Order order, bool expected)
    {
        var validator = new OrderValidator();

        var result = validator.IsValid(order);

        Assert.Equal(expected, result);
    }
}
[ClassData]

複数のテストクラスで共有するデータに最適です。

// xUnit v3: use TheoryDataRow<T> for strongly-typed rows
public class CurrencyConversionData : IEnumerable<TheoryDataRow<string, string, decimal>>
{
    public IEnumerator<TheoryDataRow<string, string, decimal>> GetEnumerator()
    {
        yield return new("USD", "EUR", 0.92m);
        yield return new("GBP", "USD", 1.27m);
        yield return new("EUR", "GBP", 0.86m);
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// xUnit v2 compatibility: v2 uses IEnumerable<object[]> instead of TheoryDataRow<T>
// public class CurrencyConversionData : IEnumerable<object[]>
// {
//     public IEnumerator<object[]> GetEnumerator()
//     {
//         yield return new object[] { "USD", "EUR", 0.92m };
//     }
//     IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// }

[Theory]
[ClassData(typeof(CurrencyConversionData))]
public void Convert_KnownPairs_ReturnsExpectedRate(
    string from, string to, decimal expectedRate)
{
    var converter = new CurrencyConverter();

    var rate = converter.GetRate(from, to);

    Assert.Equal(expectedRate, rate, precision: 2);
}

フィクスチャ:共有セットアップとティアダウン

フィクスチャを使用すると、テストの分離性を保ちながら、テスト間でコストの高いリソースを共有できます。

IClassFixture<T> — テストクラスごとの共有

同じクラス内の複数のテストで、コストの高いリソース(データベース接続、構成など)を共有する場合に使用します。

public class DatabaseFixture : IAsyncLifetime
{
    public string ConnectionString { get; private set; } = "";

    public ValueTask InitializeAsync()
    {
        // xUnit v3: returns ValueTask (v2 returns Task)
        ConnectionString = $"Host=localhost;Database=test_{Guid.NewGuid():N}";
        // Create database, run migrations, etc.
        return ValueTask.CompletedTask;
    }

    public ValueTask DisposeAsync()
    {
        // xUnit v3: returns ValueTask (v2 returns Task)
        // Drop database
        return ValueTask.CompletedTask;
    }
}

public class OrderRepositoryTests : IClassFixture<DatabaseFixture>
{
    private readonly DatabaseFixture _db;

    public OrderRepositoryTests(DatabaseFixture db)
    {
        _db = db;
        // Each test gets the shared database fixture
    }

    [Fact]
    public async Task GetById_ExistingOrder_ReturnsOrder()
    {
        var repo = new OrderRepository(_db.ConnectionString);

        var result = await repo.GetByIdAsync(KnownOrderId);

        Assert.NotNull(result);
    }
}

v2 との互換性に関する注記: xUnit v2 では、IAsyncLifetime.InitializeAsync() と DisposeAsync() は Task を返します。v3 では ValueTask を返します。移行時には、戻り値の型を適宜変更してください。

ICollectionFixture<T> — テストクラス間での共有

複数のテストクラスで同じコストの高いリソースが必要な場合に使用します。

// 1. Define the collection
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
    // This class has no code -- it is a marker for the collection
}

// 2. Use in test classes
[Collection("Database")]
public class OrderRepositoryTests
{
    private readonly DatabaseFixture _db;

    public OrderRepositoryTests(DatabaseFixture db)
    {
        _db = db;
    }

    [Fact]
    public async Task Insert_ValidOrder_Persists()
    {
        // Uses the shared database fixture
    }
}

[Collection("Database")]
public class CustomerRepositoryTests
{
    private readonly DatabaseFixture _db;

    public CustomerRepositoryTests(DatabaseFixture db)
    {
        _db = db;
    }
}
テストクラスでの IAsyncLifetime

共有フィクスチャを使わず、テストごとに非同期のセットアップ/ティアダウンを行う場合に使用します。

public class FileProcessorTests : IAsyncLifetime
{
    private string _tempDir = "";

    public ValueTask InitializeAsync()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
        Directory.CreateDirectory(_tempDir);
        return ValueTask.CompletedTask;
    }

    public ValueTask DisposeAsync()
    {
        if (Directory.Exists(_tempDir))
            Directory.Delete(_tempDir, recursive: true);
        return ValueTask.CompletedTask;
    }

    [Fact]
    public async Task Process_CsvFile_ExtractsRecords()
    {
        var filePath = Path.Combine(_tempDir, "data.csv");
        await File.WriteAllTextAsync(filePath, "Name,Age\nAlice,30\nBob,25");

        var processor = new FileProcessor();
        var records = await processor.ProcessAsync(filePath);

        Assert.Equal(2, records.Count);
    }
}

並列実行

既定の動作

xUnit は、同じコレクション内のテストクラスを順番に実行しますが、異なるコレクションは並列に実行します。明示的な [Collection] 属性が付いていない各テストクラスは、それぞれ独自の暗黙的なコレクションとなるため、既定ではテストクラスが並列に実行されます。

並列実行の制御
特定のテストの並列実行を無効にする

変更可能な状態を共有するテストは、同じコレクションに配置します。

[CollectionDefinition("Sequential", DisableParallelization = true)]
public class SequentialCollection { }

[Collection("Sequential")]
public class StatefulServiceTests
{
    // These tests run sequentially within this collection
}
アセンブリレベルの構成

テストプロジェクトのルートに xunit.runner.json を作成します。

{
    "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
    "parallelizeAssembly": false,
    "parallelizeTestCollections": true,
    "maxParallelThreads": 4
}

出力先にコピーされるように設定します。

<ItemGroup>
  <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

v2 との互換性に関する注意: v2 では、xunit.runner.json またはアセンブリ属性を使用して構成していました。v3 でも xunit.runner.json が引き続きサポートされ、プロパティ名も同じです。


テスト出力

ITestOutputHelper

テスト結果に表示される診断出力を取得します。

public class DiagnosticTests
{
    private readonly ITestOutputHelper _output;

    public DiagnosticTests(ITestOutputHelper output)
    {
        _output = output;
    }

    [Fact]
    public async Task ProcessBatch_LargeDataset_CompletesWithinTimeout()
    {
        var sw = Stopwatch.StartNew();

        var result = await processor.ProcessBatchAsync(largeDataset);

        sw.Stop();
        _output.WriteLine($"Processed {result.Count} items in {sw.ElapsedMilliseconds}ms");
        Assert.True(sw.Elapsed < TimeSpan.FromSeconds(5));
    }
}
ILogger との統合

統合テストで xUnit の出力を Microsoft.Extensions.Logging に接続します。

// NuGet: Microsoft.Extensions.Logging (for LoggerFactory)
// + a logging provider that writes to ITestOutputHelper
// Common approach: use a simple adapter
public class XunitLoggerProvider : ILoggerProvider
{
    private readonly ITestOutputHelper _output;

    public XunitLoggerProvider(ITestOutputHelper output) => _output = output;

    public ILogger CreateLogger(string categoryName) =>
        new XunitLogger(_output, categoryName);

    public void Dispose() { }
}

public class XunitLogger(ITestOutputHelper output, string category) : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
    public bool IsEnabled(LogLevel logLevel) => true;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
        Exception? exception, Func<TState, Exception?, string> formatter)
    {
        output.WriteLine($"[{logLevel}] {category}: {formatter(state, exception)}");
        if (exception is not null)
            output.WriteLine(exception.ToString());
    }
}

カスタムアサーション

カスタムメソッドで Assert を拡張する

テストコードを簡潔にするため、ドメイン固有のアサーションを作成します。

public static class OrderAssert
{
    public static void HasStatus(Order order, OrderStatus expected)
    {
        Assert.NotNull(order);
        if (order.Status != expected)
        {
            throw Xunit.Sdk.EqualException.ForMismatchedValues(
                expected, order.Status);
        }
    }

    public static void ContainsItem(Order order, string sku, int quantity)
    {
        Assert.NotNull(order);
        var item = Assert.Single(order.Items, i => i.Sku == sku);
        Assert.Equal(quantity, item.Quantity);
    }
}

// Usage
[Fact]
public void Complete_ValidOrder_SetsCompletedStatus()
{
    var order = new Order();
    order.Complete();

    OrderAssert.HasStatus(order, OrderStatus.Completed);
}
Assert.Multiple の使用(xUnit v3)

関連するアサーションをまとめ、1 つが失敗してもすべて評価されるようにします。

[Fact]
public void CreateOrder_ValidRequest_SetsAllProperties()
{
    var order = OrderFactory.Create(request);

    Assert.Multiple(
        () => Assert.Equal("cust-123", order.CustomerId),
        () => Assert.Equal(OrderStatus.Pending, order.Status),
        () => Assert.NotEqual(Guid.Empty, order.Id),
        () => Assert.NotEmpty(order.Items)
    );
}

v2 との互換性に関する注意: Assert.Multiple は xUnit v3 で新たに追加されました。v2 では、個別にアサーションを記述してください。最初の失敗時にテストが停止します。


xUnit アナライザー

xunit.analyzers パッケージ(xUnit v3 に同梱)は、テスト作成時によくあるミスをコンパイル時に検出します。

重要なルール
ルール説明重大度
xUnit1004テストメソッドをスキップしないでください情報
xUnit1012値型のパラメーターに null を使用しないでください警告
xUnit1025Theory 内で InlineData を重複させないでください警告
xUnit2000定数とリテラルには、期待される引数を指定してください警告
xUnit2002値型に null チェックを使用しないでください警告
xUnit2007型のチェックに typeof 式を使用しないでください警告
xUnit2013コレクションのサイズのチェックに等価比較を使用しないでください警告
xUnit2017セット内に値が存在するかの確認に Contains() を使用しないでください警告
特定のルールを抑制する

テストプロジェクトの .editorconfig で設定します。

[tests/**.cs]
# Allow skipped tests during development
dotnet_diagnostic.xUnit1004.severity = suggestion

重要な原則

  • [Fact] 1 つにつき事実 1 つ、[Theory] 1 つにつき概念 1 つ。 [Theory] で本質的に異なるシナリオをテストする場合は、個別の [Fact] メソッドに分けてください。
  • 単一のテストクラス内で高コストの共有リソースを使用する場合は、IClassFixture を使用します。 複数のクラスで同じリソースを共有する場合は、ICollectionFixture を使用します。
  • 並列実行をグローバルに無効にしないでください。 代わりに、変更可能な状態を共有するテストを名前付きコレクションにまとめます。
  • コンストラクターや IDisposable の代わりに、非同期のセットアップ/ティアダウンには IAsyncLifetime を使用します。 コンストラクターは非同期にできず、IDisposable.Dispose() は完了を待機しません。
  • テストデータはテストの近くに配置します。 単純なケースには [InlineData] を使用してください。データが複雑な場合や共有される場合に限り、[MemberData] または [ClassData] を使用します。
  • すべてのテストプロジェクトで xUnit アナライザーを有効にします。 誤ってテストが成功したり、テストが不安定になったりする原因となる、よくあるミスを検出できます。

エージェントで注意すべき点

  1. 静的メソッドで、コンストラクターから注入された ITestOutputHelper を使用しないでください。 ITestOutputHelper はテストインスタンスごとのものです。静的フィールドではなく、インスタンスフィールドに保存してください。
  2. フィクスチャクラスを public にするのを忘れないでください。 xUnit では、フィクスチャ型が public であり、パラメーターなしの public コンストラクター(または IAsyncLifetime)を持つ必要があります。非公開のフィクスチャは、エラーが表示されないまま失敗します。
  3. 同じメソッドに [Fact] と [Theory] を併用しないでください。 メソッドは Fact か Theory のどちらか一方であり、両方にはなりません。
  4. 非同期テストメソッドで void を返さないでください。 Task または ValueTask を返してください。xUnit は非同期処理の完了を監視できないため、async void のテストは誤って成功と報告されます。
  5. 対応する [CollectionDefinition] がない状態で [Collection] を使用しないでください。 対応するコレクション名がない場合、既定の動作で暗黙のコレクションが作成され、本来の目的が損なわれます。

参考資料

コメント