C# and .NET testing patterns with xUnit, FluentAssertions, mocking, integration tests, and test organization best practices.
origin
ECC
C# Testing Patterns
Comprehensive testing patterns for .NET applications using xUnit, FluentAssertions, and modern testing practices.
When to Activate
Writing new tests for C# code
Reviewing test quality and coverage
Setting up test infrastructure for .NET projects
Debugging flaky or slow tests
Test Framework Stack
Tool
Purpose
xUnit
Test framework (preferred for .NET)
FluentAssertions
Readable assertion syntax
NSubstitute or Moq
Mocking dependencies
Testcontainers
Real infrastructure in integration tests
WebApplicationFactory
ASP.NET Core integration tests
Bogus
Realistic test data generation
Unit Test Structure
Arrange-Act-Assert
publicsealedclassOrderServiceTests{privatereadonlyIOrderRepository_repository=Substitute.For<IOrderRepository>();privatereadonlyILogger<OrderService>_logger=Substitute.For<ILogger<OrderService>>();privatereadonlyOrderService_sut;publicOrderServiceTests(){_sut=newOrderService(_repository,_logger);} [Fact]publicasyncTaskPlaceOrderAsync_ReturnsSuccess_WhenRequestIsValid(){// Arrangevarrequest=newCreateOrderRequest{CustomerId="cust-123",Items=[newOrderItem("SKU-001",2,29.99m)]};// Actvarresult=await_sut.PlaceOrderAsync(request,CancellationToken.None);// Assertresult.IsSuccess.Should().BeTrue();result.Value.Should().NotBeNull();result.Value!.CustomerId.Should().Be("cust-123");} [Fact]publicasyncTaskPlaceOrderAsync_ReturnsFailure_WhenNoItems(){// Arrangevarrequest=newCreateOrderRequest{CustomerId="cust-123",Items=[]};// Actvarresult=await_sut.PlaceOrderAsync(request,CancellationToken.None);// Assertresult.IsSuccess.Should().BeFalse();result.Error.Should().Contain("at least one item");}}
Parameterized Tests with Theory
[Theory][InlineData("", false)][InlineData("a", false)][InlineData("ab@c.d", false)][InlineData("user@example.com", true)][InlineData("user+tag@example.co.uk", true)]publicvoidIsValidEmail_ReturnsExpected(stringemail,boolexpected){EmailValidator.IsValid(email).Should().Be(expected);}[Theory][MemberData(nameof(InvalidOrderCases))]publicasyncTaskPlaceOrderAsync_RejectsInvalidOrders(CreateOrderRequestrequest,stringexpectedError){varresult=await_sut.PlaceOrderAsync(request,CancellationToken.None);result.IsSuccess.Should().BeFalse();result.Error.Should().Contain(expectedError);}publicstaticTheoryData<CreateOrderRequest,string>InvalidOrderCases=>new(){{new(){CustomerId="",Items=[ValidItem()]},"CustomerId"},{new(){CustomerId="c1",Items=[]},"at least one item"},{new(){CustomerId="c1",Items=[new("",1,10m)]},"SKU"},};
Mocking with NSubstitute
[Fact]publicasyncTaskGetOrderAsync_ReturnsNull_WhenNotFound(){// ArrangevarorderId=Guid.NewGuid();_repository.FindByIdAsync(orderId,Arg.Any<CancellationToken>()).Returns((Order?)null);// Actvarresult=await_sut.GetOrderAsync(orderId,CancellationToken.None);// Assertresult.Should().BeNull();}[Fact]publicasyncTaskPlaceOrderAsync_PersistsOrder(){// Arrangevarrequest=ValidOrderRequest();// Actawait_sut.PlaceOrderAsync(request,CancellationToken.None);// Assert — verify the repository was calledawait_repository.Received(1).AddAsync(Arg.Is<Order>(o=>o.CustomerId==request.CustomerId),Arg.Any<CancellationToken>());}
ASP.NET Core Integration Tests
WebApplicationFactory Setup
publicsealedclassOrderApiTests:IClassFixture<WebApplicationFactory<Program>>{privatereadonlyHttpClient_client;publicOrderApiTests(WebApplicationFactory<Program>factory){_client=factory.WithWebHostBuilder(builder=>{builder.ConfigureServices(services=>{// Replace real DB with in-memory for testsservices.RemoveAll<DbContextOptions<AppDbContext>>();services.AddDbContext<AppDbContext>(options=>options.UseInMemoryDatabase("TestDb"));});}).CreateClient();} [Fact]publicasyncTaskGetOrder_Returns404_WhenNotFound(){varresponse=await_client.GetAsync($"/api/orders/{Guid.NewGuid()}");response.StatusCode.Should().Be(HttpStatusCode.NotFound);} [Fact]publicasyncTaskCreateOrder_Returns201_WithValidRequest(){varrequest=newCreateOrderRequest{CustomerId="cust-1",Items=[new("SKU-001",1,19.99m)]};varresponse=await_client.PostAsJsonAsync("/api/orders",request);response.StatusCode.Should().Be(HttpStatusCode.Created);response.Headers.Location.Should().NotBeNull();}}
publicsealedclassOrderBuilder{privatestring_customerId="cust-default";privatereadonlyList<OrderItem>_items=[new("SKU-001",1,10m)];publicOrderBuilderWithCustomer(stringcustomerId){_customerId=customerId;returnthis;}publicOrderBuilderWithItem(stringsku,intquantity,decimalprice){_items.Add(newOrderItem(sku,quantity,price));returnthis;}publicOrderBuild()=>Order.Create(_customerId,_items);}// Usage in testsvarorder=newOrderBuilder().WithCustomer("cust-vip").WithItem("SKU-PREMIUM",3,99.99m).Build();
Common Anti-Patterns
Anti-Pattern
Fix
Testing implementation details
Test behavior and outcomes
Shared mutable test state
Fresh instance per test (xUnit does this via constructors)
Thread.Sleep in async tests
Use Task.Delay with timeout, or polling helpers
Asserting on ToString() output
Assert on typed properties
One giant assertion per test
One logical assertion per test
Test names describing implementation
Name by behavior: Method_ExpectedResult_WhenCondition
Ignoring CancellationToken
Always pass and verify cancellation
Running Tests
# Run all tests
dotnet test# Run with coverage
dotnet test --collect:"XPlat Code Coverage"# Run specific project
dotnet test tests/MyApp.UnitTests/
# Filter by test name
dotnet test --filter "FullyQualifiedName~OrderService"# Watch mode during development
dotnet watch test --project tests/MyApp.UnitTests/