xUnit.net
xUnit.net
概要
xUnit.netは、.NET Framework用の無料でオープンソースのコミュニティフォーカスな単体テストツールです。堅牢な機能セットを提供してテストの記述と実行を行い、最新のC#言語機能を活用したモダンなテストフレームワークとして設計されています。2024年現在、xUnit.net v3が最新版として提供され、.NET 6.0以降をサポートしています。
詳細
主要な特徴
モダンな設計思想
- テストメソッドごとに新しいテストクラスインスタンスを作成(テスト分離の徹底)
- 属性ベースのシンプルなテスト記述
- .NET 6.0以降に最適化された実装
柔軟なテストデータ提供
[Theory]属性による理論テスト(Theory Tests)[InlineData]、[MemberData]、[ClassData]による多様なデータソース- カスタムデータ属性のサポート
強力なアサーション機能
Assert.Equal、Assert.Trueなどの直感的なアサーションメソッド- 例外テスト用の
Assert.Throws - 文字列、コレクション、型チェック用の専用アサーション
テストコレクション
- テストの並列実行制御
- 共有リソースの管理
- フィクスチャのライフサイクル管理
設定駆動型アプローチ
- 設定ファイルによる詳細なカスタマイズ
- プラグインシステムによる拡張性
- コマンドラインオプションの最小化で再現可能なテスト実行
メリット・デメリット
メリット
- テスト分離: 各テストメソッドで新しいクラスインスタンスを作成し、テスト間の依存を排除
- モダンな設計: 最新の.NET機能とC#言語構造を積極的に活用
- 豊富なテストデータ: Theory Testsによる効率的なパラメータ化テスト
- 並列実行: デフォルトで並列実行をサポートし、高速なテスト実行を実現
- 拡張性: プラグインアーキテクチャによる高い拡張性
デメリット
- 学習コスト: NUnitやMSTestからの移行時に概念の違いを理解する必要
- 設定の複雑さ: 高度な設定には深い理解が必要
- デバッグの難しさ: 並列実行により一部のデバッグシナリオが複雑化
- メモリ使用量: テストクラスインスタンスの多重作成によるメモリオーバーヘッド
参考ページ
- 公式サイト: xUnit.net
- 公式ドキュメント: xUnit.net Documentation
- GitHub: xunit/xunit
- API Reference: xUnit.net API Documentation
- NuGet: xunit Package
書き方の例
基本的なFact Test
using Xunit;
public class CalculatorTests
{
[Fact]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
// Arrange
var calculator = new Calculator();
int a = 5;
int b = 3;
int expected = 8;
// Act
int result = calculator.Add(a, b);
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void Divide_ByZero_ThrowsException()
{
// Arrange
var calculator = new Calculator();
// Act & Assert
Assert.Throws<DivideByZeroException>(() => calculator.Divide(10, 0));
}
}
Theory Tests(パラメータ化テスト)
public class MathOperationsTests
{
[Theory]
[InlineData(2, 3, 5)]
[InlineData(10, 15, 25)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
public void Add_VariousInputs_ReturnsExpectedResult(int a, int b, int expected)
{
// Arrange
var calculator = new Calculator();
// Act
int result = calculator.Add(a, b);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData(10, 2, 5)]
[InlineData(15, 3, 5)]
[InlineData(100, 10, 10)]
public void Divide_ValidInputs_ReturnsQuotient(int dividend, int divisor, int expected)
{
// Arrange
var calculator = new Calculator();
// Act
int result = calculator.Divide(dividend, divisor);
// Assert
Assert.Equal(expected, result);
}
}
MemberDataを使用したテストデータ
public class StringOperationsTests
{
public static IEnumerable<object[]> GetStringTestData()
{
yield return new object[] { "hello", "world", "hello world" };
yield return new object[] { "foo", "bar", "foo bar" };
yield return new object[] { "", "test", " test" };
yield return new object[] { "test", "", "test " };
}
[Theory]
[MemberData(nameof(GetStringTestData))]
public void Concatenate_VariousStrings_ReturnsExpectedResult(
string first, string second, string expected)
{
// Arrange
var stringProcessor = new StringProcessor();
// Act
string result = stringProcessor.Concatenate(first, second);
// Assert
Assert.Equal(expected, result);
}
public static TheoryData<string, bool> EmailValidationData =>
new TheoryData<string, bool>
{
{ "[email protected]", true },
{ "invalid-email", false },
{ "user@domain", false },
{ "[email protected]", true }
};
[Theory]
[MemberData(nameof(EmailValidationData))]
public void ValidateEmail_VariousInputs_ReturnsExpectedResult(
string email, bool expected)
{
// Arrange
var validator = new EmailValidator();
// Act
bool result = validator.IsValid(email);
// Assert
Assert.Equal(expected, result);
}
}
ClassDataを使用したテストデータ
public class CalculatorTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] { 1, 2, 3 };
yield return new object[] { 5, 10, 15 };
yield return new object[] { -1, -2, -3 };
yield return new object[] { 0, 5, 5 };
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class CalculatorClassDataTests
{
[Theory]
[ClassData(typeof(CalculatorTestData))]
public void Add_ClassDataInputs_ReturnsExpectedResult(int a, int b, int expected)
{
// Arrange
var calculator = new Calculator();
// Act
int result = calculator.Add(a, b);
// Assert
Assert.Equal(expected, result);
}
}
コレクションフィクスチャ
// フィクスチャクラス
public class DatabaseFixture : IDisposable
{
public DatabaseFixture()
{
// データベース接続の初期化
ConnectionString = "Server=localhost;Database=TestDB;";
InitializeDatabase();
}
public string ConnectionString { get; private set; }
private void InitializeDatabase()
{
// テスト用データベースのセットアップ
}
public void Dispose()
{
// リソースのクリーンアップ
}
}
// コレクション定義
[CollectionDefinition("Database collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
// このクラスにはコードは不要です。
// xUnit.netが自動的にフィクスチャを管理します。
}
// テストクラス
[Collection("Database collection")]
public class UserRepositoryTests
{
private readonly DatabaseFixture _fixture;
public UserRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void GetUser_ValidId_ReturnsUser()
{
// Arrange
var repository = new UserRepository(_fixture.ConnectionString);
// Act
var user = repository.GetUser(1);
// Assert
Assert.NotNull(user);
Assert.Equal(1, user.Id);
}
}
非同期テスト
public class AsyncOperationTests
{
[Fact]
public async Task GetDataAsync_ValidRequest_ReturnsData()
{
// Arrange
var service = new DataService();
var request = new DataRequest { Id = 123 };
// Act
var result = await service.GetDataAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(123, result.Id);
}
[Fact]
public async Task SlowOperation_Timeout_ThrowsException()
{
// Arrange
var service = new SlowService();
// Act & Assert
await Assert.ThrowsAsync<TimeoutException>(
() => service.SlowOperationAsync(TimeSpan.FromMilliseconds(100)));
}
}
カスタムアサーションとヘルパーメソッド
public class AdvancedAssertionTests
{
[Fact]
public void StringAssertions_Examples()
{
string actual = "Hello, World!";
Assert.StartsWith("Hello", actual);
Assert.EndsWith("World!", actual);
Assert.Contains("World", actual);
Assert.DoesNotContain("xyz", actual);
}
[Fact]
public void CollectionAssertions_Examples()
{
var numbers = new List<int> { 1, 2, 3, 4, 5 };
Assert.Equal(5, numbers.Count);
Assert.Contains(3, numbers);
Assert.DoesNotContain(10, numbers);
Assert.All(numbers, num => Assert.True(num > 0));
}
[Fact]
public void TypeAssertions_Examples()
{
object obj = "Hello World";
Assert.IsType<string>(obj);
Assert.IsAssignableFrom<IEnumerable<char>>(obj);
}
// カスタムアサーションヘルパー
private void AssertUserIsValid(User user)
{
Assert.NotNull(user);
Assert.False(string.IsNullOrEmpty(user.Name));
Assert.True(user.Age >= 0);
Assert.Matches(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", user.Email);
}
[Fact]
public void CreateUser_ValidData_ReturnsValidUser()
{
// Arrange
var userService = new UserService();
// Act
var user = userService.CreateUser("John Doe", 30, "[email protected]");
// Assert
AssertUserIsValid(user);
}
}
Skip属性とTraits
public class ConditionalTests
{
[Fact(Skip = "この機能は現在開発中です")]
public void FeatureUnderDevelopment_Test()
{
// このテストは実行されません
}
[Fact]
[Trait("Category", "Integration")]
[Trait("Priority", "High")]
public void DatabaseIntegration_Test()
{
// 統合テスト
}
[Fact]
[Trait("Category", "Unit")]
[Trait("Priority", "Low")]
public void UnitTest_Example()
{
// 単体テスト
}
}