Isaac.

testing

Unit Testing with xUnit

Write effective unit tests with xUnit and assertion libraries.

By Emem IsaacDecember 21, 20243 min read
#xunit#unit testing#tdd#mocking#assertions
Share:

A Simple Analogy

Unit tests are like quality checks on an assembly line. Each part is tested individually before assembly, catching defects early and cheaply.


Why Unit Testing?

  • Early detection: Find bugs before integration
  • Confidence: Refactor with assurance
  • Documentation: Tests show expected behavior
  • Regression prevention: Prevent breaking changes
  • Design improvement: Tests guide architecture

xUnit Test Structure

public class OrderServiceTests
{
    private readonly OrderService _orderService;
    private readonly MockOrderRepository _mockRepository;
    
    public OrderServiceTests()
    {
        _mockRepository = new MockOrderRepository();
        _orderService = new OrderService(_mockRepository);
    }
    
    [Fact]
    public async Task CreateOrder_WithValidRequest_ReturnsOrderId()
    {
        // Arrange
        var request = new CreateOrderRequest
        {
            CustomerId = "cust-123",
            Items = new List<OrderItem>
            {
                new OrderItem { ProductId = "p1", Quantity = 2, Price = 29.99m }
            }
        };
        
        // Act
        var result = await _orderService.CreateOrderAsync(request);
        
        // Assert
        result.Should().NotBeNull();
        result.Id.Should().NotBeEmpty();
        result.Total.Should().Be(59.98m);
    }
    
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    public async Task CreateOrder_WithEmptyCustomerId_ThrowsException(string customerId)
    {
        // Arrange
        var request = new CreateOrderRequest { CustomerId = customerId };
        
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentException>(
            () => _orderService.CreateOrderAsync(request));
    }
}

Mocking with Moq

public class PaymentServiceTests
{
    private readonly Mock<IPaymentGateway> _mockGateway;
    private readonly Mock<ILogger<PaymentService>> _mockLogger;
    private readonly PaymentService _service;
    
    public PaymentServiceTests()
    {
        _mockGateway = new Mock<IPaymentGateway>();
        _mockLogger = new Mock<ILogger<PaymentService>>();
        _service = new PaymentService(_mockGateway.Object, _mockLogger.Object);
    }
    
    [Fact]
    public async Task ProcessPayment_WithValidAmount_CallsGateway()
    {
        // Arrange
        var amount = 99.99m;
        _mockGateway
            .Setup(g => g.AuthorizeAsync(amount))
            .ReturnsAsync(new AuthorizationResult { Success = true, TransactionId = "tx-123" });
        
        // Act
        var result = await _service.ProcessPaymentAsync(amount);
        
        // Assert
        result.Should().NotBeNull();
        _mockGateway.Verify(g => g.AuthorizeAsync(amount), Times.Once);
    }
    
    [Fact]
    public async Task ProcessPayment_WhenGatewayFails_ThrowsException()
    {
        // Arrange
        _mockGateway
            .Setup(g => g.AuthorizeAsync(It.IsAny<decimal>()))
            .ThrowsAsync(new HttpRequestException("Gateway unavailable"));
        
        // Act & Assert
        await Assert.ThrowsAsync<HttpRequestException>(
            () => _service.ProcessPaymentAsync(99.99m));
    }
    
    [Fact]
    public async Task ProcessPayment_CallsLogger()
    {
        // Arrange
        _mockGateway
            .Setup(g => g.AuthorizeAsync(It.IsAny<decimal>()))
            .ReturnsAsync(new AuthorizationResult { Success = true });
        
        // Act
        await _service.ProcessPaymentAsync(99.99m);
        
        // Assert
        _mockLogger.Verify(
            l => l.Log(
                LogLevel.Information,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Payment processed")),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception, string>>()),
            Times.Once);
    }
}

Data-Driven Tests

public class DiscountCalculatorTests
{
    [Theory]
    [InlineData(100, 0.0)]   // No discount for low amounts
    [InlineData(500, 0.1)]   // 10% for $500+
    [InlineData(1000, 0.15)] // 15% for $1000+
    [InlineData(5000, 0.20)] // 20% for $5000+
    public void CalculateDiscount_WithAmount_ReturnsCorrectPercentage(decimal amount, double expected)
    {
        // Act
        var discount = DiscountCalculator.Calculate(amount);
        
        // Assert
        discount.Should().Be(expected);
    }
    
    [Theory]
    [MemberData(nameof(GetOrderData))]
    public void ProcessOrder_WithValidOrders_Succeeds(Order order, decimal expectedTotal)
    {
        // Act & Assert
        var total = OrderCalculator.CalculateTotal(order);
        total.Should().Be(expectedTotal);
    }
    
    public static TheoryData<Order, decimal> GetOrderData =>
        new()
        {
            {
                new Order { Items = new List<OrderItem>
                {
                    new OrderItem { Price = 50m, Quantity = 2 }
                }},
                100m
            },
            {
                new Order { Items = new List<OrderItem>
                {
                    new OrderItem { Price = 100m, Quantity = 1 }
                }},
                100m
            }
        };
}

Test Fixtures and Cleanup

public class DatabaseTests : IDisposable
{
    private readonly IDbContext _dbContext;
    
    public DatabaseTests()
    {
        // Setup
        var options = new DbContextOptionsBuilder<TestDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        _dbContext = new TestDbContext(options);
    }
    
    public void Dispose()
    {
        // Cleanup
        _dbContext?.Dispose();
    }
    
    [Fact]
    public async Task SaveUser_PersistsToDatabase()
    {
        // Arrange
        var user = new User { Id = 1, Name = "Alice" };
        
        // Act
        _dbContext.Users.Add(user);
        await _dbContext.SaveChangesAsync();
        
        // Assert
        var retrieved = await _dbContext.Users.FindAsync(1);
        retrieved.Name.Should().Be("Alice");
    }
}

Best Practices

  1. One assertion per test: Clear failure messages
  2. Arrange-Act-Assert: Consistent structure
  3. Descriptive names: Explain what's tested
  4. Use mocks sparingly: Test integration too
  5. Keep tests fast: Unit tests should run instantly

Related Concepts

  • NUnit (alternative)
  • Test-driven development (TDD)
  • Behavior-driven development (BDD)
  • Integration testing frameworks

Summary

xUnit provides a modern testing framework for C#. Write isolated, data-driven tests that provide confidence in code quality and prevent regressions.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours