xUnit.net

unit testing.NETC#testing frameworkopen sourcemodernTDDBDD

xUnit.net

Overview

xUnit.net is a free, open-source, community-focused unit testing tool for the .NET framework. It provides a robust set of features for writing and running tests, designed as a modern testing framework that leverages the latest C# language features. As of 2024, xUnit.net v3 is the latest version, supporting .NET 6.0 and later.

Details

Key Features

Modern Design Philosophy

  • Creates new test class instances for each test method (complete test isolation)
  • Attribute-based simple test description
  • Implementation optimized for .NET 6.0 and later

Flexible Test Data Provision

  • Theory Tests using [Theory] attribute
  • Diverse data sources via [InlineData], [MemberData], [ClassData]
  • Custom data attribute support

Powerful Assertion Capabilities

  • Intuitive assertion methods like Assert.Equal, Assert.True
  • Assert.Throws for exception testing
  • Dedicated assertions for strings, collections, and type checking

Test Collections

  • Parallel execution control
  • Shared resource management
  • Fixture lifecycle management

Configuration-Driven Approach

  • Detailed customization through configuration files
  • Extensibility via plugin system
  • Minimal command-line options for reproducible test execution

Pros and Cons

Pros

  1. Test Isolation: Creates new class instances for each test method, eliminating test dependencies
  2. Modern Design: Actively leverages the latest .NET features and C# language constructs
  3. Rich Test Data: Efficient parameterized testing through Theory Tests
  4. Parallel Execution: Default support for parallel execution enabling fast test runs
  5. Extensibility: High extensibility through plugin architecture

Cons

  1. Learning Cost: Need to understand conceptual differences when migrating from NUnit or MSTest
  2. Configuration Complexity: Advanced configuration requires deep understanding
  3. Debugging Difficulty: Parallel execution complicates some debugging scenarios
  4. Memory Usage: Memory overhead due to multiple test class instance creation

Reference Links

Code Examples

Basic 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 (Parameterized 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);
    }
}

Test Data Using 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);
    }
}

Test Data Using 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);
    }
}

Collection Fixtures

// Fixture class
public class DatabaseFixture : IDisposable
{
    public DatabaseFixture()
    {
        // Initialize database connection
        ConnectionString = "Server=localhost;Database=TestDB;";
        InitializeDatabase();
    }

    public string ConnectionString { get; private set; }

    private void InitializeDatabase()
    {
        // Setup test database
    }

    public void Dispose()
    {
        // Resource cleanup
    }
}

// Collection definition
[CollectionDefinition("Database collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
    // This class has no code, and is never created.
    // Its purpose is simply to be the place to apply [CollectionDefinition] and all the
    // ICollectionFixture<> interfaces.
}

// Test class
[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);
    }
}

Asynchronous Tests

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)));
    }
}

Custom Assertions and Helper Methods

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);
    }

    // Custom assertion helper
    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 Attribute and Traits

public class ConditionalTests
{
    [Fact(Skip = "This feature is currently under development")]
    public void FeatureUnderDevelopment_Test()
    {
        // This test will not be executed
    }

    [Fact]
    [Trait("Category", "Integration")]
    [Trait("Priority", "High")]
    public void DatabaseIntegration_Test()
    {
        // Integration test
    }

    [Fact]
    [Trait("Category", "Unit")]
    [Trait("Priority", "Low")]
    public void UnitTest_Example()
    {
        // Unit test
    }
}