NUnit

単体テスト.NETC#テストフレームワークオープンソースアサーションテスト駆動開発

NUnit

概要

NUnitは、すべての.NET言語に対応するオープンソースの単体テストフレームワークです。2024年現在、最新バージョンはNUnit 4.3.2で、.NET Frameworkから.NET Coreまで幅広い環境をサポートしています。Test-Driven Development(TDD)から本格的なシステム・統合テストまで、あらゆるテストシナリオに対応できる包括的なテストソリューションを提供します。

詳細

主要な特徴

現代的なフレームワーク設計

  • NUnit 4は最新の.NET機能とC#言語構造を活用
  • 最小サポートターゲットは.NET Framework 4.6.2および.NET 6.0
  • クロスプラットフォーム対応(Windows、macOS、Linux)

豊富なアサーション機能

  • 流暢なアサーションモデル(Fluent Assertion)
  • Assert.That()メソッドによる可読性の高いテスト記述
  • 制約ベースのアサーション(Is., Does., Has., Contains.

柔軟なテスト属性

  • [TestFixture][Test][TestCase]などの包括的な属性セット
  • パラメータ化テストの強力なサポート
  • [SetUp][TearDown]による柔軟なフィクスチャ管理

パフォーマンス向上

  • 大規模コードベースでのテスト実行速度大幅改善
  • CollectionAssert.AreEquivalentの性能問題修正
  • コレクションの深い比較の最適化

メリット・デメリット

メリット

  1. 成熟したエコシステム: 長年の開発により安定性と信頼性が確立
  2. 豊富なドキュメント: 包括的な公式ドキュメントとコミュニティサポート
  3. IDE統合: Visual Studio、Rider、VS Codeでの優れた統合
  4. 柔軟性: 単純な単体テストから複雑な統合テストまで対応
  5. 広範なサポート: すべての.NET言語とフレームワークバージョンに対応

デメリット

  1. 学習曲線: 初心者には豊富な機能が複雑に感じる場合がある
  2. セットアップの複雑さ: 大規模プロジェクトでの初期設定が煩雑
  3. パフォーマンス: 非常に大きなテストスイートでは実行時間が課題となる場合
  4. 移行コスト: NUnit 3からNUnit 4への移行には注意深い準備が必要

参考ページ

書き方の例

基本的なテスト構造

using NUnit.Framework;

[TestFixture]
public class CalculatorTests
{
    private Calculator _calculator;

    [SetUp]
    public void Setup()
    {
        _calculator = new Calculator();
    }

    [Test]
    public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
    {
        // Arrange
        int a = 5;
        int b = 3;
        int expected = 8;

        // Act
        int result = _calculator.Add(a, b);

        // Assert
        Assert.That(result, Is.EqualTo(expected));
    }
}

パラメータ化テスト

[TestFixture]
public class MathOperationsTests
{
    [TestCase(2, 3, 5)]
    [TestCase(10, 15, 25)]
    [TestCase(-1, 1, 0)]
    [TestCase(0, 0, 0)]
    public void Add_VariousInputs_ReturnsExpectedResult(int a, int b, int expected)
    {
        var calculator = new Calculator();
        int result = calculator.Add(a, b);
        Assert.That(result, Is.EqualTo(expected));
    }

    [TestCase(10, 2, ExpectedResult = 5)]
    [TestCase(15, 3, ExpectedResult = 5)]
    [TestCase(100, 10, ExpectedResult = 10)]
    public int Divide_ValidInputs_ReturnsQuotient(int dividend, int divisor)
    {
        var calculator = new Calculator();
        return calculator.Divide(dividend, divisor);
    }
}

アサーションの高度な使用

[TestFixture]
public class AdvancedAssertionTests
{
    [Test]
    public void StringAssertions_Examples()
    {
        string actual = "Hello, World!";
        
        Assert.That(actual, Does.StartWith("Hello"));
        Assert.That(actual, Does.EndWith("World!"));
        Assert.That(actual, Does.Contain("World"));
        Assert.That(actual, Is.Not.Null.And.Not.Empty);
    }

    [Test]
    public void CollectionAssertions_Examples()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        
        Assert.That(numbers, Has.Count.EqualTo(5));
        Assert.That(numbers, Contains.Item(3));
        Assert.That(numbers, Is.All.GreaterThan(0));
        Assert.That(numbers, Is.Ordered.Ascending);
    }

    [Test]
    public void ExceptionAssertions_Examples()
    {
        var calculator = new Calculator();
        
        Assert.Throws<DivideByZeroException>(() => calculator.Divide(10, 0));
        
        var ex = Assert.Throws<ArgumentException>(() => 
            calculator.CalculateSquareRoot(-1));
        Assert.That(ex.Message, Does.Contain("negative"));
    }
}

フィクスチャのライフサイクル管理

[TestFixture]
public class DatabaseTests
{
    private DatabaseConnection _connection;
    private TestDataBuilder _dataBuilder;

    [OneTimeSetUp]
    public void OneTimeSetup()
    {
        // テストフィクスチャ全体で一度だけ実行
        _connection = new DatabaseConnection("test_connection_string");
        _connection.Connect();
    }

    [SetUp]
    public void Setup()
    {
        // 各テストメソッドの前に実行
        _dataBuilder = new TestDataBuilder(_connection);
        _dataBuilder.CreateTestData();
    }

    [Test]
    public void GetUser_ValidId_ReturnsUser()
    {
        // Arrange
        int userId = _dataBuilder.CreateTestUser("John", "[email protected]");
        var userService = new UserService(_connection);

        // Act
        var user = userService.GetUser(userId);

        // Assert
        Assert.That(user, Is.Not.Null);
        Assert.That(user.Name, Is.EqualTo("John"));
        Assert.That(user.Email, Is.EqualTo("[email protected]"));
    }

    [TearDown]
    public void TearDown()
    {
        // 各テストメソッドの後に実行
        _dataBuilder.CleanupTestData();
    }

    [OneTimeTearDown]
    public void OneTimeTearDown()
    {
        // テストフィクスチャ全体で一度だけ実行
        _connection.Disconnect();
    }
}

非同期テスト

[TestFixture]
public class AsyncTests
{
    [Test]
    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.That(result, Is.Not.Null);
        Assert.That(result.Id, Is.EqualTo(123));
    }

    [Test]
    public void AsyncOperation_Timeout_ThrowsException()
    {
        var service = new SlowService();
        
        Assert.ThrowsAsync<TimeoutException>(async () => 
            await service.SlowOperationAsync(TimeSpan.FromMilliseconds(100)));
    }
}

カテゴリとタグによるテスト分類

[TestFixture]
[Category("Integration")]
public class IntegrationTests
{
    [Test]
    [Category("Database")]
    [Category("Slow")]
    public void DatabaseIntegration_Test()
    {
        // データベース統合テスト
    }

    [Test]
    [Category("API")]
    public void ApiIntegration_Test()
    {
        // API統合テスト
    }
}

[TestFixture]
[Category("Unit")]
public class UnitTests
{
    [Test]
    [Category("Fast")]
    public void Unit_Test()
    {
        // 高速な単体テスト
    }
}