Test Patterns and Best Practices
Common patterns for writing maintainable, reliable tests in .NET.
Test Data Builders
Object Mother Pattern
public static class OrderMother
{
public static Order CreateDefault() => new()
{
Id = 1,
CustomerId = 100,
OrderDate = DateTime.UtcNow,
TotalAmount = 99.99m,
Items = new List<OrderItem>
{
new() { ProductId = 1, Quantity = 2, Price = 49.99m }
}
};
public static Order CreateWithMultipleItems() => CreateDefault() with
{
Items = new List<OrderItem>
{
new() { ProductId = 1, Quantity = 1, Price = 29.99m },
new() { ProductId = 2, Quantity = 2, Price = 19.99m }
}
};
}
// Usage
[Test]
public void ProcessOrder_ValidOrder_Success()
{
var order = OrderMother.CreateDefault();
var result = _orderService.Process(order);
Assert.That(result.IsSuccess, Is.True);
}
Test Data Builder Pattern
public class OrderBuilder
{
private int _id = 1;
private int _customerId = 100;
private DateTime _orderDate = DateTime.UtcNow;
private List<OrderItem> _items = new();
public OrderBuilder WithId(int id)
{
_id = id;
return this;
}
public OrderBuilder WithCustomer(int customerId)
{
_customerId = customerId;
return this;
}
public OrderBuilder WithItem(int productId, int quantity, decimal price)
{
_items.Add(new OrderItem { ProductId = productId, Quantity = quantity, Price = price });
return this;
}
public Order Build() => new()
{
Id = _id,
CustomerId = _customerId,
OrderDate = _orderDate,
Items = _items
};
}
// Usage
[Test]
public void Test()
{
var order = new OrderBuilder()
.WithCustomer(200)
.WithItem(productId: 1, quantity: 2, price: 49.99m)
.WithItem(productId: 2, quantity: 1, price: 29.99m)
.Build();
}
AutoFixture
[Test]
public void AutoFixtureExample()
{
var fixture = new Fixture();
// Generate random data
var order = fixture.Create<Order>();
// Customize specific properties
fixture.Customize<Order>(c => c
.With(o => o.CustomerId, 100)
.Without(o => o.Items));
var customOrder = fixture.Create<Order>();
}
Test Fixtures
NUnit SetUp/TearDown
[TestFixture]
public class OrderServiceTests
{
private OrderService _service;
private Mock<IOrderRepository> _mockRepo;
[SetUp]
public void SetUp()
{
_mockRepo = new Mock<IOrderRepository>();
_service = new OrderService(_mockRepo.Object);
}
[TearDown]
public void TearDown()
{
// Cleanup if needed
}
[Test]
public void Test1() { }
}
Class Fixtures (xUnit)
public class DatabaseFixture : IDisposable
{
public ApplicationDbContext DbContext { get; }
public DatabaseFixture()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase("TestDb")
.Options;
DbContext = new ApplicationDbContext(options);
DbContext.Database.EnsureCreated();
}
public void Dispose()
{
DbContext.Database.EnsureDeleted();
DbContext.Dispose();
}
}
[CollectionDefinition("Database collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }
[Collection("Database collection")]
public class OrderTests
{
private readonly DatabaseFixture _fixture;
public OrderTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task Test()
{
await _fixture.DbContext.Orders.AddAsync(new Order());
await _fixture.DbContext.SaveChangesAsync();
}
}
Mocking Patterns
Verify Interactions
[Test]
public void SendEmail_ValidOrder_EmailSent()
{
var mockEmailService = new Mock<IEmailService>();
var service = new OrderService(mockEmailService.Object);
var order = OrderMother.CreateDefault();
service.Process(order);
mockEmailService.Verify(
x => x.SendAsync(It.IsAny<string>(), It.IsAny<string>()),
Times.Once);
}
Argument Matching
mockEmailService.Verify(
x => x.SendAsync(
It.Is<string>(email => email.Contains("@")),
It.IsAny<string>()),
Times.Once);
Parameterized Tests
NUnit TestCase
[TestCase(0, ExpectedResult = 0)]
[TestCase(1, ExpectedResult = 1)]
[TestCase(5, ExpectedResult = 120)]
public int Factorial_ValidInput_ReturnsCorrectValue(int n)
{
return MathHelper.Factorial(n);
}
xUnit Theory
[Theory]
[InlineData(0, 0)]
[InlineData(1, 1)]
[InlineData(5, 120)]
public void Factorial_Theory(int input, int expected)
{
var result = MathHelper.Factorial(input);
Assert.Equal(expected, result);
}
Async Testing
[Test]
public async Task ProcessOrderAsync_ValidOrder_CompletesSuccessfully()
{
var mockRepo = new Mock<IOrderRepository>();
mockRepo.Setup(x => x.SaveAsync(It.IsAny<Order>()))
.ReturnsAsync(true);
var service = new OrderService(mockRepo.Object);
var order = OrderMother.CreateDefault();
var result = await service.ProcessAsync(order);
Assert.That(result.IsSuccess, Is.True);
}
Test Naming Conventions
Method_Scenario_ExpectedResult
[Test]
public void CreateOrder_NullItems_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateOrder(null));
}
[Test]
public void CalculateTotal_MultipleItems_ReturnsSumOfPrices()
{
var order = new OrderBuilder()
.WithItem(1, 1, 10m)
.WithItem(2, 2, 20m)
.Build();
var total = _service.CalculateTotal(order);
Assert.That(total, Is.EqualTo(50m));
}
AAA Pattern
[Test]
public void Example()
{
// Arrange
var order = OrderMother.CreateDefault();
var service = new OrderService();
// Act
var result = service.Process(order);
// Assert
Assert.That(result.IsSuccess, Is.True);
}