While unit tests form the foundation of a solid testing strategy, modern applications require more sophisticated testing approaches. Let's explore advanced testing patterns that ensure your C# applications are truly production-ready.
Integration Testing with WebApplicationFactory
Test your entire application stack including HTTP routing, middleware, and database interactions:
csharp1public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> 2 where TStartup : class 3{ 4 protected override void ConfigureWebHost(IWebHostBuilder builder) 5 { 6 builder.ConfigureServices(services => 7 { 8 // Remove the app's ApplicationDbContext registration 9 var descriptor = services.SingleOrDefault( 10 d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); 11 12 if (descriptor != null) 13 services.Remove(descriptor); 14 15 // Add ApplicationDbContext using an in-memory database for testing 16 services.AddDbContext<ApplicationDbContext>(options => 17 { 18 options.UseInMemoryDatabase("InMemoryDbForTesting"); 19 }); 20 21 // Build the service provider 22 var sp = services.BuildServiceProvider(); 23 24 // Create a scope to obtain a reference to the database context 25 using var scope = sp.CreateScope(); 26 var scopedServices = scope.ServiceProvider; 27 var db = scopedServices.GetRequiredService<ApplicationDbContext>(); 28 29 // Ensure the database is created 30 db.Database.EnsureCreated(); 31 32 // Seed the database with test data 33 SeedTestData(db); 34 }); 35 } 36 37 private static void SeedTestData(ApplicationDbContext context) 38 { 39 context.Users.AddRange( 40 new User { Id = 1, Name = "Test User 1", Email = "test1@example.com" }, 41 new User { Id = 2, Name = "Test User 2", Email = "test2@example.com" } 42 ); 43 44 context.SaveChanges(); 45 } 46}
Integration Test Example
csharp1public class UserControllerIntegrationTests : IClassFixture<CustomWebApplicationFactory<Startup>> 2{ 3 private readonly HttpClient _client; 4 private readonly CustomWebApplicationFactory<Startup> _factory; 5 6 public UserControllerIntegrationTests(CustomWebApplicationFactory<Startup> factory) 7 { 8 _factory = factory; 9 _client = factory.CreateClient(); 10 } 11 12 [Fact] 13 public async Task GetUsers_ReturnsSuccessAndCorrectContentType() 14 { 15 // Act 16 var response = await _client.GetAsync("/api/users"); 17 18 // Assert 19 response.EnsureSuccessStatusCode(); 20 Assert.Equal("application/json; charset=utf-8", 21 response.Content.Headers.ContentType?.ToString()); 22 } 23 24 [Fact] 25 public async Task CreateUser_WithValidData_ReturnsCreatedUser() 26 { 27 // Arrange 28 var newUser = new CreateUserRequest 29 { 30 Name = "New User", 31 Email = "newuser@example.com" 32 }; 33 34 var content = new StringContent( 35 JsonSerializer.Serialize(newUser), 36 Encoding.UTF8, 37 "application/json"); 38 39 // Act 40 var response = await _client.PostAsync("/api/users", content); 41 42 // Assert 43 response.EnsureSuccessStatusCode(); 44 var responseContent = await response.Content.ReadAsStringAsync(); 45 var createdUser = JsonSerializer.Deserialize<User>(responseContent); 46 47 Assert.Equal(newUser.Name, createdUser?.Name); 48 Assert.Equal(newUser.Email, createdUser?.Email); 49 } 50}
Contract Testing with Pact.NET
Ensure service compatibility in microservice architectures:
csharp1public class UserServiceContractTests : IDisposable 2{ 3 private readonly PactBuilder _pactBuilder; 4 private readonly IMockProviderService _mockProviderService; 5 6 public UserServiceContractTests() 7 { 8 var pactConfig = new PactConfig 9 { 10 SpecificationVersion = "2.0.0", 11 PactDir = "../../../pacts/", 12 LogDir = "../../../logs/" 13 }; 14 15 _pactBuilder = new PactBuilder(pactConfig) 16 .ServiceConsumer("UserWebApp") 17 .HasPactWith("UserService"); 18 19 _mockProviderService = _pactBuilder.MockService(9222); 20 } 21 22 [Fact] 23 public async Task GetUser_WhenUserExists_ReturnsUserData() 24 { 25 // Arrange 26 var userId = 1; 27 var expectedUser = new 28 { 29 id = userId, 30 name = "John Doe", 31 email = "john@example.com" 32 }; 33 34 _mockProviderService 35 .Given("User with ID 1 exists") 36 .UponReceiving("A GET request for user 1") 37 .With(new ProviderServiceRequest 38 { 39 Method = HttpVerb.Get, 40 Path = $"/api/users/{userId}", 41 Headers = new Dictionary<string, object> 42 { 43 { "Accept", "application/json" } 44 } 45 }) 46 .WillRespondWith(new ProviderServiceResponse 47 { 48 Status = 200, 49 Headers = new Dictionary<string, object> 50 { 51 { "Content-Type", "application/json; charset=utf-8" } 52 }, 53 Body = expectedUser 54 }); 55 56 // Act 57 var userService = new UserService("http://localhost:9222"); 58 var result = await userService.GetUserAsync(userId); 59 60 // Assert 61 Assert.Equal(expectedUser.id, result.Id); 62 Assert.Equal(expectedUser.name, result.Name); 63 Assert.Equal(expectedUser.email, result.Email); 64 65 _mockProviderService.VerifyInteractions(); 66 } 67 68 public void Dispose() 69 { 70 _pactBuilder.Build(); 71 } 72}
Property-Based Testing with FsCheck
Test properties and invariants rather than specific examples:
csharp1[Property] 2public Property ReverseListTwice_ShouldReturnOriginal(int[] original) 3{ 4 return (original.Reverse().Reverse().SequenceEqual(original)) 5 .ToProperty(); 6} 7 8[Property] 9public Property AddingItemToList_ShouldIncreaseCount(int[] original, int newItem) 10{ 11 var list = original.ToList(); 12 var originalCount = list.Count; 13 14 list.Add(newItem); 15 16 return (list.Count == originalCount + 1).ToProperty(); 17} 18 19[Property] 20public Property SortedList_ShouldBeInOrder(int[] unsorted) 21{ 22 var sorted = unsorted.OrderBy(x => x).ToArray(); 23 24 return IsInAscendingOrder(sorted).ToProperty(); 25} 26 27private static bool IsInAscendingOrder(int[] array) 28{ 29 for (int i = 1; i < array.Length; i++) 30 { 31 if (array[i] < array[i - 1]) 32 return false; 33 } 34 return true; 35} 36 37// Custom generators for domain objects 38public class UserGenerator 39{ 40 public static Arbitrary<User> GenerateUser() 41 { 42 return Arb.From( 43 from name in Arb.Generate<NonEmptyString>() 44 from email in GenerateEmail() 45 from age in Gen.Choose(18, 100) 46 select new User 47 { 48 Name = name.Get, 49 Email = email, 50 Age = age 51 }); 52 } 53 54 private static Gen<string> GenerateEmail() 55 { 56 return from localPart in Arb.Generate<NonEmptyString>() 57 from domain in Gen.Elements("gmail.com", "outlook.com", "company.com") 58 select $"{localPart.Get}@{domain}"; 59 } 60}
Mutation Testing with Stryker.NET
Verify the quality of your tests by introducing mutations:
bash1# Install Stryker.NET 2dotnet tool install -g dotnet-stryker 3 4# Run mutation testing 5dotnet stryker --project MyProject.csproj --test-project MyProject.Tests.csproj
Configure mutation testing:
json1// stryker-config.json 2{ 3 "stryker-config": { 4 "test-runner": "dotnet", 5 "project": "src/MyProject.csproj", 6 "test-projects": ["tests/MyProject.Tests.csproj"], 7 "reporters": ["html", "console"], 8 "mutation-level": "Complete", 9 "threshold-high": 80, 10 "threshold-low": 60, 11 "excluded-mutations": ["string", "regex"] 12 } 13}
Performance Testing
Test performance characteristics of your code:
csharp1[MemoryDiagnoser] 2[SimpleJob(RuntimeMoniker.Net60)] 3public class PerformanceBenchmarks 4{ 5 private readonly List<int> _data; 6 7 public PerformanceBenchmarks() 8 { 9 _data = Enumerable.Range(1, 10000).ToList(); 10 } 11 12 [Benchmark] 13 public int ForLoop() 14 { 15 var sum = 0; 16 for (int i = 0; i < _data.Count; i++) 17 { 18 sum += _data[i]; 19 } 20 return sum; 21 } 22 23 [Benchmark] 24 public int LinqSum() 25 { 26 return _data.Sum(); 27 } 28 29 [Benchmark] 30 public int ForeachLoop() 31 { 32 var sum = 0; 33 foreach (var item in _data) 34 { 35 sum += item; 36 } 37 return sum; 38 } 39} 40 41// Performance test integration 42public class DatabasePerformanceTests 43{ 44 [Fact] 45 public async Task BulkInsert_Should_CompleteWithin_ExpectedTime() 46 { 47 // Arrange 48 var stopwatch = Stopwatch.StartNew(); 49 var items = GenerateTestItems(10000); 50 51 // Act 52 await _repository.BulkInsertAsync(items); 53 stopwatch.Stop(); 54 55 // Assert 56 Assert.True(stopwatch.ElapsedMilliseconds < 5000, 57 $"Bulk insert took {stopwatch.ElapsedMilliseconds}ms, expected < 5000ms"); 58 } 59}
Test Data Builders
Create flexible test data with the Builder pattern:
csharp1public class UserBuilder 2{ 3 private string _name = "Default Name"; 4 private string _email = "default@example.com"; 5 private int _age = 25; 6 private List<Role> _roles = new(); 7 8 public UserBuilder WithName(string name) 9 { 10 _name = name; 11 return this; 12 } 13 14 public UserBuilder WithEmail(string email) 15 { 16 _email = email; 17 return this; 18 } 19 20 public UserBuilder WithAge(int age) 21 { 22 _age = age; 23 return this; 24 } 25 26 public UserBuilder WithRole(Role role) 27 { 28 _roles.Add(role); 29 return this; 30 } 31 32 public UserBuilder AsAdmin() 33 { 34 return WithRole(Role.Admin); 35 } 36 37 public User Build() 38 { 39 return new User 40 { 41 Name = _name, 42 Email = _email, 43 Age = _age, 44 Roles = _roles.ToList() 45 }; 46 } 47 48 public static implicit operator User(UserBuilder builder) 49 { 50 return builder.Build(); 51 } 52} 53 54// Usage in tests 55[Test] 56public void AdminUser_CanAccessAdminPanel() 57{ 58 // Arrange 59 User adminUser = new UserBuilder() 60 .WithName("Admin User") 61 .WithEmail("admin@company.com") 62 .AsAdmin(); 63 64 // Act & Assert 65 Assert.True(adminUser.CanAccessAdminPanel()); 66}
Test Fixtures and Shared Context
Manage test data and setup efficiently:
csharp1public class DatabaseFixture : IDisposable 2{ 3 public ApplicationDbContext Context { get; private set; } 4 5 public DatabaseFixture() 6 { 7 var options = new DbContextOptionsBuilder<ApplicationDbContext>() 8 .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) 9 .Options; 10 11 Context = new ApplicationDbContext(options); 12 Context.Database.EnsureCreated(); 13 14 SeedData(); 15 } 16 17 private void SeedData() 18 { 19 Context.Users.AddRange( 20 new UserBuilder().WithName("User 1").WithEmail("user1@test.com"), 21 new UserBuilder().WithName("User 2").WithEmail("user2@test.com").AsAdmin() 22 ); 23 24 Context.SaveChanges(); 25 } 26 27 public void Dispose() 28 { 29 Context.Dispose(); 30 } 31} 32 33[Collection("Database collection")] 34public class UserServiceTests 35{ 36 private readonly DatabaseFixture _fixture; 37 38 public UserServiceTests(DatabaseFixture fixture) 39 { 40 _fixture = fixture; 41 } 42 43 [Fact] 44 public async Task GetUser_ReturnsCorrectUser() 45 { 46 // Arrange 47 var service = new UserService(_fixture.Context); 48 49 // Act & Assert 50 var user = await service.GetUserByEmailAsync("user1@test.com"); 51 Assert.NotNull(user); 52 Assert.Equal("User 1", user.Name); 53 } 54} 55 56[CollectionDefinition("Database collection")] 57public class DatabaseCollection : ICollectionFixture<DatabaseFixture> 58{ 59 // This class has no code, and is never created. Its purpose is simply 60 // to be the place to apply [CollectionDefinition] and all the 61 // ICollectionFixture<> interfaces. 62}
Conclusion
Advanced testing patterns in C# go far beyond simple unit tests. By implementing integration testing, contract testing, property-based testing, and performance testing, you create a comprehensive safety net that ensures your applications work correctly under all conditions.
Remember to balance test coverage with maintainability, and choose the right testing approach for each scenario. A well-tested application is not just about code coverage—it's about confidence in your system's behavior across all possible scenarios.