Unit Testing with xUnit
Write effective unit tests with xUnit and assertion libraries.
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
- One assertion per test: Clear failure messages
- Arrange-Act-Assert: Consistent structure
- Descriptive names: Explain what's tested
- Use mocks sparingly: Test integration too
- 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.
Related Articles
Unit Testing Best Practices
Write effective unit tests that catch bugs and enable refactoring.
Read More aspnetUnit Testing in ASP.NET Core
Learn how to write effective unit tests for ASP.NET Core applications using xUnit, Moq, and best practices.
Read More testingIntegration Testing with ASP.NET Core
Write comprehensive integration tests for real-world scenarios.
Read More