Tests

Advanced C# Testing Patterns: Beyond Unit Tests with Integration and Contract Testing

Explore advanced testing patterns in C# including integration testing, contract testing, property-based testing, and mutation testing for robust applications.

I
Isaiah Clifford Opoku
Dec 8, 20246 min read
#csharp#testing#unit-tests
6 min
reading time
Advanced C# Testing Patterns: Beyond Unit Tests with Integration and Contract Testing

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:

csharp
1public 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

csharp
1public 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:

csharp
1public 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:

csharp
1[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:

bash
1# 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:

json
1// 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:

csharp
1[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:

csharp
1public 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:

csharp
1public 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.

Related Technologies

Technologies and tools featured in this article

#

csharp

#

testing

#

unit-tests

#

integration-tests

#

dotnet

5
Technologies
Tests
Category
6m
Read Time

Continue Reading

Discover more insights and technical articles from our collection

Back to all posts

Found this helpful?

I'm posting .NET and software engineering content on multiple Social Media platforms.

If you enjoyed this article and want to see more content like this, consider subscribing to my newsletter or following me on social media for the latest updates.