Test Architecture and Strategy
Overview
Test architecture defines how tests are organized, executed, and maintained. A well-designed testing strategy ensures adequate coverage while optimizing for speed, reliability, and maintainability.
Table of Contents
- Testing Pyramid
- Testing Trophy
- Test Categories
- When to Write Each Test Type
- Test Coverage Strategies
- Contract Testing
- Mutation Testing
- Testing Microservices
- Testing Event-Driven Systems
- QA vs Developer Testing
- Test Organization
- Test Data Management
- Interview Questions
Testing Pyramid
The testing pyramid is a metaphor for the ideal distribution of different test types.
/\
/ \
/ E2E\ < Few, slow, expensive
/------\
/ Integ \ < Some, medium speed
/----------\
/ Unit \ < Many, fast, cheap
/--------------\
Distribution Guidelines
| Test Type | % of Tests | Execution Time | Scope |
|---|---|---|---|
| Unit | 70% | Milliseconds | Single function/class |
| Integration | 20% | Seconds | Component interactions |
| E2E | 10% | Minutes | Full user workflows |
Unit Tests
// Fast, isolated, many
[Fact]
public void CalculateDiscount_PremiumCustomer_Returns20Percent()
{
var calculator = new DiscountCalculator();
var result = calculator.Calculate(100m, CustomerType.Premium);
Assert.Equal(20m, result);
}
Characteristics:
- Test single units (methods, functions)
- No external dependencies (database, network)
- Run in milliseconds
- Provide immediate feedback
Integration Tests
// Test component interactions
[Fact]
public async Task CreateOrder_SavesOrderToDatabase()
{
await using var context = CreateTestDbContext();
var repository = new OrderRepository(context);
var service = new OrderService(repository);
var order = new Order { CustomerId = 1, Total = 100m };
await service.CreateAsync(order);
var saved = await context.Orders.FindAsync(order.Id);
Assert.NotNull(saved);
}
Characteristics:
- Test multiple components together
- May use real database (in-memory or containers)
- Run in seconds
- Catch integration bugs
E2E Tests
// Test complete user workflows
[Fact]
public async Task Checkout_CompletePurchaseFlow()
{
await Page.GotoAsync("/products");
await Page.GetByTestId("product-1").ClickAsync();
await Page.GetByRole(AriaRole.Button, new() { Name = "Add to Cart" }).ClickAsync();
await Page.GetByRole(AriaRole.Link, new() { Name = "Checkout" }).ClickAsync();
await FillShippingInfo();
await FillPaymentInfo();
await Page.GetByRole(AriaRole.Button, new() { Name = "Place Order" }).ClickAsync();
await Expect(Page.GetByText("Order Confirmed")).ToBeVisibleAsync();
}
Characteristics:
- Test full user journeys
- Use real browser automation
- Run in minutes
- Verify end-to-end behavior
Testing Trophy
An alternative to the pyramid, emphasizing integration tests.
_______
/ Static\
/ Types \
/___________\
| |
| Integration | < Primary focus
|_____________|
/ \
/ Unit \
/___________________|
| |
| E2E |
|_____________________|
Trophy Distribution
| Test Type | Emphasis | Rationale |
|---|---|---|
| Static | High | Types prevent many bugs at compile time |
| Integration | Highest | Best confidence-to-effort ratio |
| Unit | Medium | For complex business logic |
| E2E | Low | Critical paths only |
When to Prefer Trophy
- Full-stack applications with complex integrations
- Microservices where boundaries matter
- Rapid development where integration bugs are common
- Brownfield projects with limited test coverage
Test Categories
Functional Testing
// Tests what the system does
[Fact]
public void CreateOrder_WithValidData_ReturnsOrderId()
{
var service = new OrderService();
var order = new CreateOrderRequest { CustomerId = 1 };
var result = service.Create(order);
Assert.NotEqual(Guid.Empty, result.OrderId);
}
Non-Functional Testing
Performance Testing
[Fact]
public void CalculateTotal_LargeOrder_CompletesWithinThreshold()
{
var calculator = new OrderCalculator();
var items = Enumerable.Range(1, 10000)
.Select(i => new OrderItem { Price = 10m, Quantity = 1 })
.ToList();
var sw = Stopwatch.StartNew();
calculator.CalculateTotal(items);
sw.Stop();
Assert.True(sw.ElapsedMilliseconds < 100, "Should complete in under 100ms");
}
Load Testing (with k6)
// k6 script
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 100,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('https://api.example.com/orders');
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
Security Testing
[Fact]
public async Task Login_SqlInjectionAttempt_ReturnsUnauthorized()
{
var client = _factory.CreateClient();
var payload = new LoginRequest
{
Email = "'; DROP TABLE Users; --",
Password = "password"
};
var response = await client.PostAsJsonAsync("/api/auth/login", payload);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
// Verify database wasn't affected
}
Regression Testing
// Automated suite run before releases
[Collection("Regression")]
public class OrderRegressionTests
{
[Fact]
public async Task Legacy_OrderCreation_StillWorks() { }
[Fact]
public async Task Legacy_Discount_CalculatesCorrectly() { }
[Fact]
public async Task Legacy_EmailNotification_Sends() { }
}
Smoke Testing
// Quick sanity checks after deployment
[Collection("Smoke")]
public class SmokeTests
{
[Fact]
public async Task HealthEndpoint_ReturnsOk()
{
var response = await _client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task Database_IsConnected()
{
var response = await _client.GetAsync("/health/db");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task Homepage_Loads()
{
var response = await _client.GetAsync("/");
Assert.True(response.IsSuccessStatusCode);
}
}
When to Write Each Test Type
Decision Matrix
| Scenario | Unit | Integration | E2E |
|---|---|---|---|
| Pure business logic | Primary | - | - |
| Data transformations | Primary | - | - |
| API endpoint behavior | - | Primary | Backup |
| Database operations | Mock | Primary | - |
| External service calls | Mock | Primary | - |
| Critical user flows | - | - | Primary |
| UI interactions | - | - | Primary |
| Cross-service communication | - | Primary | Backup |
| Authentication flows | - | Primary | Primary |
The Test Decision Flowchart
Is it pure business logic?
โโโ Yes โ Unit test
โโโ No โ Does it involve external systems?
โโโ Yes โ Integration test (mock if expensive)
โโโ No โ Is it a critical user journey?
โโโ Yes โ E2E test
โโโ No โ Integration or unit based on complexity
Examples by Feature
Order Discount Calculation
// Unit test - Pure logic
[Theory]
[InlineData(CustomerType.Regular, 0)]
[InlineData(CustomerType.Premium, 10)]
[InlineData(CustomerType.VIP, 20)]
public void GetDiscountPercent_ByCustomerType(CustomerType type, int expected)
{
var discount = DiscountCalculator.GetPercent(type);
Assert.Equal(expected, discount);
}
Order API Endpoint
// Integration test - API + Database
[Fact]
public async Task CreateOrder_ValidOrder_SavesAndReturnsCreated()
{
var client = _factory.CreateClient();
var order = new CreateOrderRequest { CustomerId = 1, Items = ... };
var response = await client.PostAsJsonAsync("/api/orders", order);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
Complete Checkout Flow
// E2E test - Full user journey
[Fact]
public async Task User_CanCompleteCheckout()
{
await LoginAsTestUser();
await AddProductToCart("SKU-123");
await ProceedToCheckout();
await CompletePayment();
await VerifyOrderConfirmation();
}
Test Coverage Strategies
Types of Coverage
| Metric | Description | Target |
|---|---|---|
| Line Coverage | Lines executed | 70-80% |
| Branch Coverage | Decision paths | 60-70% |
| Method Coverage | Methods called | 80-90% |
| Condition Coverage | Boolean sub-expressions | 50-60% |
| Path Coverage | Unique execution paths | Impractical for 100% |
Coverage Tools
# Using Coverlet
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
# Generate HTML report
reportgenerator -reports:coverage.cobertura.xml -targetdir:coverage-report
Coverage Configuration
<!-- Directory.Build.props -->
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<ExcludeByAttribute>
GeneratedCodeAttribute,
ExcludeFromCodeCoverageAttribute
</ExcludeByAttribute>
<ExcludeByFile>
**/*.Designer.cs,
**/Migrations/*.cs
</ExcludeByFile>
</PropertyGroup>
Coverage Anti-Patterns
// DON'T: Test trivial code just for coverage
public class PersonTests
{
[Fact]
public void FullName_ReturnsFirstAndLastName()
{
var person = new Person { FirstName = "John", LastName = "Doe" };
Assert.Equal("John Doe", person.FullName); // Testing a simple concatenation
}
}
// DON'T: Write tests that don't assert anything meaningful
[Fact]
public void Constructor_DoesNotThrow()
{
var service = new OrderService(); // No assertion, just coverage
}
// DO: Focus on behavior, not implementation
[Fact]
public void CalculateShipping_FreeForOrdersOver100()
{
var result = _calculator.CalculateShipping(orderTotal: 150m);
Assert.Equal(0m, result);
}
Risk-Based Coverage
Prioritize coverage based on:
- Business criticality: Payment processing > user preferences
- Change frequency: Often-modified code needs more tests
- Complexity: Complex algorithms need thorough testing
- Historical bugs: Areas with past issues need regression tests
Contract Testing
What is Contract Testing?
Contract testing verifies that services communicate correctly by testing against a shared contract rather than the actual service.
Consumer Contract Provider
โ โ โ
โโโ Generates โโโโโโบโ โ
โ โโโโ Validates โโโโโโค
โ โ โ
Consumer-Driven Contract Testing with Pact
Consumer Side
// Install: dotnet add package PactNet
public class OrderServiceConsumerTests
{
private readonly IPactBuilderV4 _pactBuilder;
public OrderServiceConsumerTests()
{
var pact = Pact.V4("OrderAPI-Consumer", "OrderAPI-Provider", new PactConfig());
_pactBuilder = pact.WithHttpInteractions();
}
[Fact]
public async Task GetOrder_ReturnsOrder()
{
var orderId = Guid.Parse("12345678-1234-1234-1234-123456789012");
_pactBuilder
.UponReceiving("A request for an order")
.Given("Order exists")
.WithRequest(HttpMethod.Get, $"/api/orders/{orderId}")
.WillRespond()
.WithStatus(HttpStatusCode.OK)
.WithJsonBody(new
{
id = orderId,
customerId = 1,
total = 100.00m,
status = "Pending"
});
await _pactBuilder.VerifyAsync(async ctx =>
{
var client = new HttpClient { BaseAddress = ctx.MockServerUri };
var orderClient = new OrderApiClient(client);
var order = await orderClient.GetOrderAsync(orderId);
Assert.Equal(orderId, order.Id);
Assert.Equal(100.00m, order.Total);
});
}
}
Provider Side
public class OrderServiceProviderTests
{
[Fact]
public void VerifyPactWithConsumer()
{
var config = new PactVerifierConfig();
var pactVerifier = new PactVerifier(config);
pactVerifier
.ServiceProvider("OrderAPI-Provider", new Uri("http://localhost:5000"))
.WithPactBrokerSource(new Uri("https://pact-broker.example.com"))
.WithProviderStateUrl(new Uri("http://localhost:5000/provider-states"))
.Verify();
}
}
Benefits of Contract Testing
- Fast feedback: No need to deploy full environment
- Independent deployment: Teams can deploy without coordination
- Breaking change detection: Catch incompatibilities early
- Documentation: Contracts serve as API documentation
Mutation Testing
What is Mutation Testing?
Mutation testing evaluates test quality by introducing bugs (mutations) and checking if tests detect them.
Original Code Mutant Test Result
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโ
if (x > 0) โ if (x >= 0) Killed/Survived?
return x + y; โ return x - y; Killed/Survived?
Using Stryker.NET
# Install
dotnet tool install -g dotnet-stryker
# Run
dotnet stryker --project src/MyProject.csproj
Configuration
// stryker-config.json
{
"stryker-config": {
"project": "MyProject.csproj",
"solution": "MySolution.sln",
"test-projects": ["MyProject.Tests.csproj"],
"mutate": ["!**/bin/**", "!**/obj/**"],
"reporters": ["html", "progress", "dashboard"],
"thresholds": {
"high": 80,
"low": 60,
"break": 50
}
}
}
Mutation Operators
| Operator | Original | Mutant |
|---|---|---|
| Arithmetic | a + b |
a - b |
| Relational | a > b |
a >= b |
| Logical | a && b |
a || b |
| Negation | if (x) |
if (!x) |
| Return | return true |
return false |
Interpreting Results
Mutation Score = Killed Mutations / Total Mutations ร 100%
Score Interpretation
โโโโโโโโโโโโโโโโโโโโโโโโ
90%+ Excellent test quality
70-90% Good test quality
50-70% Needs improvement
<50% Poor test quality
Testing Microservices
Testing Strategies
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ End-to-End Tests โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โ Contract โ โ Contract โ
โ Tests โ โ Tests โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Service A โ โ Service B โ โ Service C โ
โ โโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโ โ
โ โ Integration โ โ โ โ Integration โ โ โ โ Integration โ โ
โ โโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโ โ
โ โ Unit โ โ โ โ Unit โ โ โ โ Unit โ โ
โ โโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
Service Testing
// Test service in isolation with mocked dependencies
public class OrderServiceTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public OrderServiceTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace external service clients with mocks
services.AddSingleton<IPaymentServiceClient>(
Substitute.For<IPaymentServiceClient>());
services.AddSingleton<IInventoryServiceClient>(
Substitute.For<IInventoryServiceClient>());
});
});
}
[Fact]
public async Task CreateOrder_CallsPaymentAndInventory()
{
var client = _factory.CreateClient();
// Test service behavior with mocked dependencies
}
}
Component Testing
// Test component with real database but mocked external services
public class OrderComponentTests : IClassFixture<OrderServiceFactory>
{
[Fact]
public async Task OrderFlow_FromCreationToCompletion()
{
// Test complete component behavior
await CreateOrder();
await ProcessPayment();
await UpdateInventory();
await SendNotification();
await VerifyOrderState();
}
}
Cross-Service Testing
// Use Docker Compose for multi-service tests
public class CrossServiceTests : IAsyncLifetime
{
private DockerComposeCompositeService _compose;
public async Task InitializeAsync()
{
_compose = new DockerComposeCompositeService(
new DirectoryInfo("."),
new[]
{
"docker-compose.yml",
"docker-compose.test.yml"
});
await _compose.StartAsync();
}
[Fact]
public async Task OrderCreation_TriggersInventoryReservation()
{
// Test cross-service behavior
}
public async Task DisposeAsync()
{
await _compose.StopAsync();
}
}
Testing Event-Driven Systems
Event Testing Patterns
Testing Event Publishers
public class OrderServiceEventTests
{
[Fact]
public async Task CreateOrder_PublishesOrderCreatedEvent()
{
// Arrange
var events = new List<IEvent>();
var eventBus = Substitute.For<IEventBus>();
eventBus.PublishAsync(Arg.Do<IEvent>(e => events.Add(e)));
var service = new OrderService(eventBus);
// Act
await service.CreateOrderAsync(new CreateOrderRequest());
// Assert
Assert.Single(events);
Assert.IsType<OrderCreatedEvent>(events[0]);
}
}
Testing Event Handlers
public class PaymentReceivedHandlerTests
{
[Fact]
public async Task Handle_UpdatesOrderStatus()
{
// Arrange
var repository = new FakeOrderRepository();
var handler = new PaymentReceivedHandler(repository);
var order = new Order { Id = Guid.NewGuid(), Status = OrderStatus.Pending };
await repository.SaveAsync(order);
var @event = new PaymentReceivedEvent { OrderId = order.Id };
// Act
await handler.HandleAsync(@event);
// Assert
var updated = await repository.GetByIdAsync(order.Id);
Assert.Equal(OrderStatus.Paid, updated.Status);
}
}
Testing Event Flows
public class EventFlowTests
{
[Fact]
public async Task OrderFlow_FromCreationToShipment()
{
// Use in-memory event bus for testing
var eventBus = new InMemoryEventBus();
var handlers = new EventHandlerRegistry();
// Register handlers
handlers.Register<OrderCreatedEvent>(new InventoryHandler());
handlers.Register<InventoryReservedEvent>(new PaymentHandler());
handlers.Register<PaymentReceivedEvent>(new ShippingHandler());
// Trigger flow
await eventBus.PublishAsync(new OrderCreatedEvent { OrderId = orderId });
// Wait for processing
await eventBus.WaitForCompletion(TimeSpan.FromSeconds(5));
// Verify final state
var order = await _orderRepository.GetByIdAsync(orderId);
Assert.Equal(OrderStatus.Shipped, order.Status);
}
}
Saga Testing
public class OrderSagaTests
{
[Fact]
public async Task OrderSaga_PaymentFails_CompensatesInventory()
{
// Arrange
var saga = new OrderSaga(
_orderService,
_inventoryService,
_paymentService);
_paymentService.ProcessAsync(Arg.Any<Payment>())
.ThrowsAsync(new PaymentFailedException());
// Act
await saga.ExecuteAsync(new CreateOrderCommand());
// Assert - Compensation was called
await _inventoryService.Received().ReleaseReservationAsync(Arg.Any<Guid>());
await _orderService.Received().CancelAsync(Arg.Any<Guid>());
}
}
QA vs Developer Testing
Responsibility Matrix
| Testing Type | Developer | QA |
|---|---|---|
| Unit Tests | Primary | Review |
| Integration Tests | Primary | Augment |
| API Tests | Primary | Augment |
| E2E Tests | Support | Primary |
| Exploratory Testing | - | Primary |
| Performance Testing | Support | Primary |
| Security Testing | Support | Primary |
| Accessibility Testing | Awareness | Primary |
Shift-Left Testing
Traditional:
Code โ Code Review โ QA Testing โ Bug Fixes โ Release
โ
Late discovery
Expensive fixes
Shift-Left:
Code + Tests โ Code Review โ Automated Tests โ Release
โ
Early discovery
Cheap fixes
Test Ownership
// Developer-owned: Unit and integration tests
namespace OrderService.Tests
{
public class OrderValidatorTests { }
public class OrderRepositoryTests { }
public class OrderApiTests { }
}
// QA-owned: E2E and exploratory test scenarios
namespace OrderService.E2ETests
{
public class CheckoutFlowTests { }
public class EdgeCaseScenarios { }
}
// Shared: Regression test suites
namespace OrderService.RegressionTests
{
public class CriticalPathTests { }
}
Test Organization
Project Structure
Solution/
โโโ src/
โ โโโ OrderService/
โ โ โโโ Controllers/
โ โ โโโ Services/
โ โ โโโ Models/
โ โโโ OrderService.Infrastructure/
โโโ tests/
โ โโโ OrderService.UnitTests/
โ โ โโโ Services/
โ โ โ โโโ OrderServiceTests.cs
โ โ โโโ Validators/
โ โ โโโ OrderValidatorTests.cs
โ โโโ OrderService.IntegrationTests/
โ โ โโโ Api/
โ โ โ โโโ OrdersControllerTests.cs
โ โ โโโ Repositories/
โ โ โโโ OrderRepositoryTests.cs
โ โโโ OrderService.E2ETests/
โ โ โโโ PageObjects/
โ โ โโโ Flows/
โ โโโ OrderService.PerformanceTests/
โโโ test-infrastructure/
โโโ docker-compose.test.yml
โโโ TestContainerSetup.cs
Test Categorization
// Using traits/categories
[Trait("Category", "Unit")]
public class OrderValidatorTests { }
[Trait("Category", "Integration")]
public class OrderRepositoryTests { }
[Trait("Category", "E2E")]
public class CheckoutFlowTests { }
[Trait("Category", "Smoke")]
public class HealthCheckTests { }
// Running specific categories
// dotnet test --filter "Category=Unit"
// dotnet test --filter "Category!=E2E"
Test Fixtures
// Shared setup for related tests
public class DatabaseFixture : IAsyncLifetime
{
public ApplicationDbContext Context { get; private set; }
public async Task InitializeAsync()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
Context = new ApplicationDbContext(options);
await SeedDataAsync();
}
public Task DisposeAsync()
{
Context?.Dispose();
return Task.CompletedTask;
}
private async Task SeedDataAsync()
{
Context.Customers.Add(new Customer { Id = 1, Name = "Test Customer" });
await Context.SaveChangesAsync();
}
}
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }
[Collection("Database")]
public class OrderRepositoryTests
{
private readonly DatabaseFixture _fixture;
public OrderRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
}
Test Data Management
Test Data Patterns
Builder Pattern
public class OrderBuilder
{
private readonly Order _order = new()
{
Id = Guid.NewGuid(),
Status = OrderStatus.Pending,
CreatedAt = DateTime.UtcNow
};
public OrderBuilder WithCustomer(int customerId)
{
_order.CustomerId = customerId;
return this;
}
public OrderBuilder WithItem(string productId, int quantity, decimal price)
{
_order.Items.Add(new OrderItem
{
ProductId = productId,
Quantity = quantity,
UnitPrice = price
});
return this;
}
public OrderBuilder AsPaid()
{
_order.Status = OrderStatus.Paid;
_order.PaidAt = DateTime.UtcNow;
return this;
}
public Order Build() => _order;
}
// Usage
var order = new OrderBuilder()
.WithCustomer(1)
.WithItem("PROD-1", 2, 10.00m)
.AsPaid()
.Build();
Object Mother Pattern
public static class TestOrders
{
public static Order PendingOrder() =>
new OrderBuilder().WithItem("PROD-1", 1, 10.00m).Build();
public static Order PaidOrder() =>
new OrderBuilder().WithItem("PROD-1", 1, 10.00m).AsPaid().Build();
public static Order LargeOrder() =>
new OrderBuilder()
.WithItem("PROD-1", 100, 10.00m)
.WithItem("PROD-2", 50, 20.00m)
.Build();
}
// Usage
var order = TestOrders.PaidOrder();
Fixture Files
// tests/fixtures/orders.json
public class TestDataLoader
{
public static T Load<T>(string fixture)
{
var path = Path.Combine("fixtures", $"{fixture}.json");
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<T>(json)!;
}
}
// Usage
var orders = TestDataLoader.Load<List<Order>>("orders");
Database Seeding
public class TestDataSeeder
{
public static async Task SeedAsync(ApplicationDbContext context)
{
// Clear existing data
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
// Seed reference data
var customers = new[]
{
new Customer { Id = 1, Name = "Test Customer", Type = CustomerType.Regular },
new Customer { Id = 2, Name = "Premium Customer", Type = CustomerType.Premium }
};
context.Customers.AddRange(customers);
var products = new[]
{
new Product { Id = "PROD-1", Name = "Widget", Price = 10.00m },
new Product { Id = "PROD-2", Name = "Gadget", Price = 25.00m }
};
context.Products.AddRange(products);
await context.SaveChangesAsync();
}
}
Data Isolation
// Transaction-based isolation
public class TransactionalTest : IAsyncLifetime
{
private IDbContextTransaction _transaction;
public async Task InitializeAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task DisposeAsync()
{
await _transaction.RollbackAsync();
}
}
// Database-per-test isolation
[Fact]
public async Task IsolatedTest()
{
var dbName = Guid.NewGuid().ToString();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(dbName)
.Options;
using var context = new ApplicationDbContext(options);
// Test with isolated database
}
Interview Questions
1. What is the testing pyramid and when might you deviate from it?
Answer: The testing pyramid suggests having many unit tests, fewer integration tests, and fewest E2E tests. This optimizes for fast feedback and low maintenance.
Deviation scenarios:
- Testing trophy: When integration tests provide better confidence-to-cost ratio
- Legacy systems: May need more E2E tests when unit testing is difficult
- UI-heavy applications: May need more E2E tests for visual validation
- Microservices: Contract tests may be more valuable than unit tests
2. How do you decide what type of test to write?
Answer:
- Pure logic: Unit test
- Component interactions: Integration test
- Database operations: Integration with real/in-memory database
- External services: Integration with mocks, contract tests for verification
- Critical user journeys: E2E tests
- Complex UI: E2E with visual comparison
Key principle: Test at the lowest level that gives confidence.
3. What is contract testing and when would you use it?
Answer: Contract testing verifies that services communicate correctly by testing against a shared contract rather than the actual service.
Use cases:
- Microservices: Verify API compatibility between services
- Team independence: Allow independent deployment without integration testing everything
- Breaking change detection: Catch API incompatibilities early
- Documentation: Contracts serve as living API documentation
Tools: Pact, Spring Cloud Contract
4. How do you test event-driven systems?
Answer:
- Unit test handlers: Test each handler in isolation
- Test event publishing: Verify correct events are published
- Test event flows: Use in-memory event bus to verify complete flows
- Saga testing: Verify compensating actions on failures
- Contract testing: Verify event schema compatibility
Key challenges:
- Async nature requires waiting/polling
- Order of events may vary
- Need to test compensation/rollback scenarios
5. What code coverage target should a project have?
Answer: Coverage targets should be risk-based:
- 80% for business-critical code (payments, security)
- 70% for general business logic
- 50-60% for infrastructure/boilerplate
Important considerations:
- Coverage measures execution, not correctness
- Focus on meaningful tests, not coverage numbers
- Use mutation testing to validate test quality
- Some code (trivial getters, generated code) shouldnโt need tests
Key Takeaways
- Test pyramid: Start with unit tests, add integration and E2E as needed
- Risk-based testing: Focus on business-critical paths
- Contract testing: Essential for microservices independence
- Mutation testing: Validates your tests actually catch bugs
- Shift-left: Test early, test often
- Test data: Use builders and fixtures for consistent test data
- Organization: Clear separation between test types