Loading content...
Repositories interact with external databases—by definition, they have side effects and dependencies on infrastructure. This makes testing them more nuanced than testing pure domain logic. How do you write tests that are fast, reliable, and actually verify that your persistence layer works?
The answer involves understanding the testing spectrum: from ultra-fast unit tests with mocks, through in-memory database tests, to full integration tests against real databases. Each approach has its place, and knowing when to use which is a hallmark of experienced engineers.
By the end of this page, you will understand the complete testing strategy for repositories: when to use mocks vs. in-memory databases vs. real databases, how to set up effective test fixtures, what to test and what not to test, and how to balance test speed with test confidence.
Repository tests fall on a spectrum from isolated unit tests to full integration tests. Understanding this spectrum helps you choose the right approach for what you're trying to verify.
| Approach | Speed | Confidence | What It Tests | Best For |
|---|---|---|---|---|
| Mock/Fake Repository | Fastest (ms) | Low for repo | Domain logic using repository | Testing services that USE repositories |
| In-Memory Database | Fast (10-100ms) | Medium | ORM mappings, query logic | Testing repository implementations |
| Real Database (Containerized) | Slower (100ms-1s) | High | Full stack including DB | Verifying production behavior |
| Real Database (Shared) | Slowest | Highest | Production-like environment | CI/CD integration tests |
Most repository tests should use in-memory databases—they're fast enough for TDD and reliable enough to catch real bugs. Mock repositories are for testing code that USES repositories. Real database tests are for critical scenarios and CI verification. Don't put all tests at one level.
A Balanced Testing Strategy:
/\
/ \ Few: Real database integration tests
/ \ (CI/CD, critical paths)
/------\
/ \ Many: In-memory database tests
/ \ (Repository implementations)
/────────────
/ \ Most: Mocks for services that use repositories
/__________________\ (Fast unit tests for domain/service logic)
Let's explore each level in detail.
When testing code that uses a repository (services, handlers, domain logic), you should mock or fake the repository. This isolates the code under test and runs blazingly fast.
Mock vs Fake:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
using Moq;using Xunit; public class PlaceOrderServiceTests{ private readonly Mock<IOrderRepository> _orderRepoMock; private readonly Mock<ICustomerRepository> _customerRepoMock; private readonly PlaceOrderService _service; public PlaceOrderServiceTests() { _orderRepoMock = new Mock<IOrderRepository>(); _customerRepoMock = new Mock<ICustomerRepository>(); _service = new PlaceOrderService( _orderRepoMock.Object, _customerRepoMock.Object); } [Fact] public async Task PlaceOrder_ValidCustomer_CreatesOrderAndAddsToRepository() { // Arrange var customerId = new CustomerId(Guid.NewGuid()); var customer = new Customer(customerId, "John Doe", Email.From("john@example.com")); _customerRepoMock .Setup(r => r.GetByIdAsync(customerId, It.IsAny<CancellationToken>())) .ReturnsAsync(customer); var command = new PlaceOrderCommand( CustomerId: customerId, Items: new[] { new OrderItemDto("SKU-001", 2, 29.99m) }); // Act var orderId = await _service.PlaceOrder(command); // Assert Assert.NotNull(orderId); // Verify Add was called with an order containing correct data _orderRepoMock.Verify( r => r.Add(It.Is<Order>(o => o.CustomerId == customerId && o.LineItems.Count == 1)), Times.Once); } [Fact] public async Task PlaceOrder_CustomerNotFound_ThrowsException() { // Arrange var customerId = new CustomerId(Guid.NewGuid()); _customerRepoMock .Setup(r => r.GetByIdAsync(customerId, It.IsAny<CancellationToken>())) .ReturnsAsync((Customer?)null); var command = new PlaceOrderCommand(customerId, Array.Empty<OrderItemDto>()); // Act & Assert await Assert.ThrowsAsync<CustomerNotFoundException>( () => _service.PlaceOrder(command)); // Verify Add was never called _orderRepoMock.Verify(r => r.Add(It.IsAny<Order>()), Times.Never); } [Fact] public async Task PlaceOrder_DuplicateIdempotencyKey_ThrowsException() { // Arrange var customerId = new CustomerId(Guid.NewGuid()); var customer = new Customer(customerId, "John Doe", Email.From("john@example.com")); _customerRepoMock .Setup(r => r.GetByIdAsync(customerId, It.IsAny<CancellationToken>())) .ReturnsAsync(customer); _orderRepoMock .Setup(r => r.ExistsWithIdempotencyKey("unique-key-123")) .Returns(true); // Already exists var command = new PlaceOrderCommand( CustomerId: customerId, Items: new[] { new OrderItemDto("SKU-001", 1, 19.99m) }, IdempotencyKey: "unique-key-123"); // Act & Assert await Assert.ThrowsAsync<DuplicateOrderException>( () => _service.PlaceOrder(command)); }}Mocks verify that your service calls repository methods correctly. They do NOT verify that your repository actually works—that it maps entities correctly, that queries return expected results, or that transactions commit. For that, you need in-memory or integration tests on the repository itself.
A fake repository is a fully functional in-memory implementation. It's more reusable than mocks and can be shared across tests. Fakes are especially useful when multiple tests need a realistic repository that maintains state across operations.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
/// <summary>/// In-memory implementation of IOrderRepository for testing./// Maintains state like a real repository but uses Dictionary storage./// </summary>public class FakeOrderRepository : IOrderRepository{ private readonly Dictionary<OrderId, Order> _orders = new(); private long _nextOrderNumber = 1000; public Order? GetById(OrderId id) { return _orders.TryGetValue(id, out var order) ? order : null; } public Order GetByIdOrThrow(OrderId id) { return GetById(id) ?? throw new OrderNotFoundException(id); } public Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default) { return Task.FromResult(GetById(id)); } public void Add(Order order) { if (_orders.ContainsKey(order.Id)) throw new DuplicateOrderException(order.Id, new Exception("Fake duplicate")); _orders[order.Id] = order; } public void Remove(Order order) { _orders.Remove(order.Id); } public IEnumerable<Order> FindOrdersReadyForShipment() { return _orders.Values .Where(o => o.Status == OrderStatus.Paid && o.StockVerified && !o.ShippedAt.HasValue) .OrderBy(o => o.OrderDate) .ToList(); } public IEnumerable<Order> FindOrdersPendingPayment() { return _orders.Values .Where(o => o.Status == OrderStatus.PendingPayment) .OrderBy(o => o.OrderDate) .ToList(); } public IEnumerable<Order> FindForCustomer(CustomerId customerId) { return _orders.Values .Where(o => o.CustomerId == customerId) .OrderByDescending(o => o.OrderDate) .ToList(); } public bool ExistsWithIdempotencyKey(string key) { return _orders.Values.Any(o => o.IdempotencyKey == key); } public OrderNumber GenerateNextOrderNumber() { return new OrderNumber(Interlocked.Increment(ref _nextOrderNumber)); } // Test helper methods public void Clear() => _orders.Clear(); public int Count => _orders.Count; public void Seed(IEnumerable<Order> orders) { foreach (var order in orders) _orders[order.Id] = order; }} // Usage in testspublic class OrderProcessingTests{ private readonly FakeOrderRepository _orderRepo; private readonly ShipmentService _service; public OrderProcessingTests() { _orderRepo = new FakeOrderRepository(); _service = new ShipmentService(_orderRepo); } [Fact] public void ProcessReadyOrders_ShipsAllQualifyingOrders() { // Arrange - Seed with test data var readyOrder1 = CreatePaidVerifiedOrder(); var readyOrder2 = CreatePaidVerifiedOrder(); var pendingOrder = CreatePendingOrder(); // Should not be shipped _orderRepo.Seed(new[] { readyOrder1, readyOrder2, pendingOrder }); // Act var shippedCount = _service.ProcessReadyOrders(); // Assert Assert.Equal(2, shippedCount); Assert.True(_orderRepo.GetById(readyOrder1.Id)!.ShippedAt.HasValue); Assert.True(_orderRepo.GetById(readyOrder2.Id)!.ShippedAt.HasValue); Assert.False(_orderRepo.GetById(pendingOrder.Id)!.ShippedAt.HasValue); }}Keep fake logic simple—use LINQ on collections to mirror query behavior. Add test helper methods (Seed, Clear, Count) to make test setup easier. Throw the same exceptions as real repositories (DuplicateOrderException) to test error handling paths. Consider using the same fake in both unit and acceptance tests for consistency.
To test the actual repository implementation—the one that uses Entity Framework, Hibernate, or another ORM—you need a database. In-memory databases provide fast, isolated testing without external dependencies.
Options:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
using Microsoft.EntityFrameworkCore;using Xunit; public class EfOrderRepositoryTests : IDisposable{ private readonly OrderingDbContext _context; private readonly EfOrderRepository _repository; public EfOrderRepositoryTests() { // Create in-memory database with unique name for test isolation var options = new DbContextOptionsBuilder<OrderingDbContext>() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; _context = new OrderingDbContext(options); _repository = new EfOrderRepository(_context, new NullLogger<EfOrderRepository>()); } [Fact] public void GetById_ExistingOrder_ReturnsOrderWithLineItems() { // Arrange var order = CreateTestOrder(); order.AddLineItem(new LineItem("SKU-001", 2, Money.FromDecimal(29.99m, "USD"))); order.AddLineItem(new LineItem("SKU-002", 1, Money.FromDecimal(49.99m, "USD"))); _context.Orders.Add(order); _context.SaveChanges(); _context.ChangeTracker.Clear(); // Clear tracking to simulate fresh load // Act var retrieved = _repository.GetById(order.Id); // Assert Assert.NotNull(retrieved); Assert.Equal(order.Id, retrieved.Id); Assert.Equal(2, retrieved.LineItems.Count); // Verifies eager loading works } [Fact] public void GetById_NonExistingOrder_ReturnsNull() { // Arrange var nonExistentId = new OrderId(Guid.NewGuid()); // Act var retrieved = _repository.GetById(nonExistentId); // Assert Assert.Null(retrieved); } [Fact] public void Add_NewOrder_PersistsSuccessfully() { // Arrange var order = CreateTestOrder(); // Act _repository.Add(order); _context.SaveChanges(); // Assert _context.ChangeTracker.Clear(); var persisted = _context.Orders.Find(order.Id); Assert.NotNull(persisted); } [Fact] public void FindOrdersReadyForShipment_OnlyReturnsQualifyingOrders() { // Arrange var readyOrder = CreateTestOrder(); readyOrder.MarkAsPaid(); readyOrder.VerifyStock(); var notPaidOrder = CreateTestOrder(); // Status is still Pending var paidButNotVerifiedOrder = CreateTestOrder(); paidButNotVerifiedOrder.MarkAsPaid(); // Stock not verified var alreadyShippedOrder = CreateTestOrder(); alreadyShippedOrder.MarkAsPaid(); alreadyShippedOrder.VerifyStock(); alreadyShippedOrder.MarkAsShipped(); _context.Orders.AddRange( readyOrder, notPaidOrder, paidButNotVerifiedOrder, alreadyShippedOrder); _context.SaveChanges(); // Act var readyOrders = _repository.FindOrdersReadyForShipment().ToList(); // Assert Assert.Single(readyOrders); Assert.Equal(readyOrder.Id, readyOrders[0].Id); } [Fact] public void ExistsWithIdempotencyKey_DuplicateKey_ReturnsTrue() { // Arrange var order = CreateTestOrder(idempotencyKey: "unique-key-abc"); _context.Orders.Add(order); _context.SaveChanges(); // Act var exists = _repository.ExistsWithIdempotencyKey("unique-key-abc"); // Assert Assert.True(exists); } public void Dispose() { _context.Dispose(); } private static Order CreateTestOrder(string? idempotencyKey = null) { return new Order( new OrderId(Guid.NewGuid()), new CustomerId(Guid.NewGuid()), DateTime.UtcNow, idempotencyKey); }}EF Core's InMemory provider doesn't enforce foreign keys, unique constraints, or computed columns. For tests that depend on these features, use SQLite in-memory mode or Testcontainers with a real database. Always verify critical constraints in integration tests against a production-like database.
SQLite in-memory mode provides a real SQL database that enforces constraints, making it superior to pure in-memory providers for catching issues early.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
using Microsoft.Data.Sqlite;using Microsoft.EntityFrameworkCore;using Xunit; public class SqliteOrderRepositoryTests : IDisposable{ private readonly SqliteConnection _connection; private readonly OrderingDbContext _context; private readonly EfOrderRepository _repository; public SqliteOrderRepositoryTests() { // Create an in-memory SQLite database // Connection string uses DataSource=:memory: for in-memory database // Mode=ReadWriteCreate to allow creating tables _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); // Keep connection open for lifetime of test var options = new DbContextOptionsBuilder<OrderingDbContext>() .UseSqlite(_connection) .Options; _context = new OrderingDbContext(options); _context.Database.EnsureCreated(); // Create tables from model _repository = new EfOrderRepository(_context, new NullLogger<EfOrderRepository>()); } [Fact] public void Add_DuplicateOrderId_ThrowsException() { // Arrange var orderId = new OrderId(Guid.NewGuid()); var order1 = CreateTestOrderWithId(orderId); var order2 = CreateTestOrderWithId(orderId); // Same ID _repository.Add(order1); _context.SaveChanges(); // Act & Assert _repository.Add(order2); // SQLite enforces primary key constraint Assert.Throws<DbUpdateException>(() => _context.SaveChanges()); } [Fact] public void Update_ConcurrentModification_ThrowsConcurrencyException() { // Arrange - requires RowVersion/Concurrency token on Order entity var order = CreateTestOrder(); _context.Orders.Add(order); _context.SaveChanges(); // Simulate concurrent modification using (var context2 = CreateContext()) { var orderCopy = context2.Orders.Find(order.Id)!; orderCopy.AddLineItem(new LineItem("SKU-X", 1, Money.USD(10))); context2.SaveChanges(); } // Act - try to save from original context order.AddLineItem(new LineItem("SKU-Y", 1, Money.USD(20))); // Assert Assert.Throws<DbUpdateConcurrencyException>(() => _context.SaveChanges()); } [Fact] public void FindForCustomer_OrderByDescendingDate_ReturnsCorrectOrder() { // Arrange var customerId = new CustomerId(Guid.NewGuid()); var oldOrder = CreateTestOrder(customerId, DateTime.UtcNow.AddDays(-10)); var middleOrder = CreateTestOrder(customerId, DateTime.UtcNow.AddDays(-5)); var newOrder = CreateTestOrder(customerId, DateTime.UtcNow); // Add in random order _context.Orders.AddRange(middleOrder, newOrder, oldOrder); _context.SaveChanges(); _context.ChangeTracker.Clear(); // Act var orders = _repository.FindForCustomer(customerId).ToList(); // Assert - should be newest first Assert.Equal(3, orders.Count); Assert.Equal(newOrder.Id, orders[0].Id); Assert.Equal(middleOrder.Id, orders[1].Id); Assert.Equal(oldOrder.Id, orders[2].Id); } private OrderingDbContext CreateContext() { var options = new DbContextOptionsBuilder<OrderingDbContext>() .UseSqlite(_connection) .Options; return new OrderingDbContext(options); } public void Dispose() { _context.Dispose(); _connection.Dispose(); }}Use SQLite in-memory when you need: primary key constraints, foreign key enforcement, unique constraints, ordering/sorting verification, concurrency token testing. Use EF Core InMemory when you need maximum speed and only care about basic CRUD behavior.
For maximum confidence, test against the same database engine you use in production. Testcontainers makes this manageable by spinning up real databases in Docker containers during test runs.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
using Testcontainers.PostgreSql;using Xunit; public class PostgresOrderRepositoryTests : IAsyncLifetime{ private PostgreSqlContainer _postgres = null!; private OrderingDbContext _context = null!; private EfOrderRepository _repository = null!; public async Task InitializeAsync() { // Start a PostgreSQL container _postgres = new PostgreSqlBuilder() .WithImage("postgres:15-alpine") .WithDatabase("ordering_tests") .WithUsername("test") .WithPassword("test") .Build(); await _postgres.StartAsync(); // Configure EF Core to use the container var options = new DbContextOptionsBuilder<OrderingDbContext>() .UseNpgsql(_postgres.GetConnectionString()) .Options; _context = new OrderingDbContext(options); await _context.Database.MigrateAsync(); // Apply migrations _repository = new EfOrderRepository(_context, new NullLogger<EfOrderRepository>()); } public async Task DisposeAsync() { await _context.DisposeAsync(); await _postgres.DisposeAsync(); } [Fact] public async Task GenerateNextOrderNumber_UsesDatabaseSequence() { // Arrange - sequence is created by migration // Act var number1 = _repository.GenerateNextOrderNumber(); var number2 = _repository.GenerateNextOrderNumber(); var number3 = _repository.GenerateNextOrderNumber(); // Assert - numbers are sequential Assert.True(number2.Value > number1.Value); Assert.True(number3.Value > number2.Value); } [Fact] public async Task Add_OrderWithLineItems_CreatesRecordsInAllTables() { // Arrange var order = CreateTestOrder(); order.AddLineItem(new LineItem("SKU-001", 2, Money.FromDecimal(29.99m, "USD"))); order.AddLineItem(new LineItem("SKU-002", 1, Money.FromDecimal(49.99m, "USD"))); // Act _repository.Add(order); await _context.SaveChangesAsync(); // Assert - verify using raw SQL that records exist await using var connection = new NpgsqlConnection(_postgres.GetConnectionString()); await connection.OpenAsync(); var orderCount = await connection.ExecuteScalarAsync<int>( "SELECT COUNT(*) FROM orders WHERE id = @id", new { id = order.Id.Value }); Assert.Equal(1, orderCount); var lineItemCount = await connection.ExecuteScalarAsync<int>( "SELECT COUNT(*) FROM order_line_items WHERE order_id = @id", new { id = order.Id.Value }); Assert.Equal(2, lineItemCount); } [Fact] public async Task FindOrdersReadyForShipment_UsesIndex() { // This test verifies that the query plan uses expected indexes // Important for performance-critical queries // Seed enough data to make indexes relevant for (int i = 0; i < 100; i++) { var order = CreateTestOrder(); if (i % 10 == 0) { order.MarkAsPaid(); order.VerifyStock(); } _context.Orders.Add(order); } await _context.SaveChangesAsync(); _context.ChangeTracker.Clear(); // Use EXPLAIN ANALYZE to verify index usage await using var connection = new NpgsqlConnection(_postgres.GetConnectionString()); await connection.OpenAsync(); var explainResult = await connection.QueryFirstAsync<string>( @"EXPLAIN (ANALYZE, FORMAT TEXT) SELECT * FROM orders WHERE status = 'Paid' AND stock_verified = true AND shipped_at IS NULL"); // Assert index is used (look for Index Scan vs Seq Scan) Assert.Contains("Index", explainResult); }}Use real database tests for: database sequences, stored procedures, database-specific features (JSON queries, full-text search), index usage verification, migration testing, and any behavior that differs between databases. These tests are slower but catch issues that in-memory tests miss.
Not every repository method needs exhaustive testing. Focus your testing effort where it provides the most value.
Before writing a repository test, ask: 'If this test passed but the code was wrong, would we have a production bug?' If yes, the test is valuable. If the test would only fail due to ORM misconfiguration (which would break everything), it might not be worth isolated testing.
Creating test data is often the most tedious part of repository testing. Object Mothers and Test Data Builders make this manageable.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
/// <summary>/// Test Data Builder for Order aggregate./// Provides fluent API for creating test orders with sensible defaults./// </summary>public class OrderBuilder{ private OrderId _id = new OrderId(Guid.NewGuid()); private CustomerId _customerId = new CustomerId(Guid.NewGuid()); private DateTime _orderDate = DateTime.UtcNow; private OrderStatus _status = OrderStatus.Pending; private bool _stockVerified = false; private DateTime? _shippedAt = null; private string? _idempotencyKey = null; private readonly List<LineItem> _lineItems = new(); private Address? _shippingAddress = null; public static OrderBuilder AnOrder() => new(); public OrderBuilder WithId(OrderId id) { _id = id; return this; } public OrderBuilder WithId(Guid id) { _id = new OrderId(id); return this; } public OrderBuilder ForCustomer(CustomerId customerId) { _customerId = customerId; return this; } public OrderBuilder PlacedOn(DateTime date) { _orderDate = date; return this; } public OrderBuilder PlacedDaysAgo(int days) { _orderDate = DateTime.UtcNow.AddDays(-days); return this; } public OrderBuilder WithStatus(OrderStatus status) { _status = status; return this; } public OrderBuilder Paid() { _status = OrderStatus.Paid; return this; } public OrderBuilder Pending() { _status = OrderStatus.Pending; return this; } public OrderBuilder StockVerified() { _stockVerified = true; return this; } public OrderBuilder Shipped() { _shippedAt = DateTime.UtcNow; return this; } public OrderBuilder WithIdempotencyKey(string key) { _idempotencyKey = key; return this; } public OrderBuilder WithLineItem(string sku, int quantity, decimal price) { _lineItems.Add(new LineItem(sku, quantity, Money.FromDecimal(price, "USD"))); return this; } public OrderBuilder WithShippingAddress(Address address) { _shippingAddress = address; return this; } // Convenience method for "ready for shipment" state public OrderBuilder ReadyForShipment() { return Paid().StockVerified(); } public Order Build() { var order = new Order(_id, _customerId, _orderDate, _idempotencyKey); // Apply status changes if (_status == OrderStatus.Paid) order.MarkAsPaid(); if (_stockVerified) order.VerifyStock(); if (_shippedAt.HasValue) order.MarkAsShipped(); // Add line items foreach (var item in _lineItems) order.AddLineItem(item); // Set addresses if (_shippingAddress != null) order.SetShippingAddress(_shippingAddress); return order; }} // Usage in tests - clean and expressivepublic class OrderRepositoryTests{ [Fact] public void FindOrdersReadyForShipment_ReturnsOnlyQualifyingOrders() { // Arrange - builders make intent clear var ready1 = OrderBuilder.AnOrder() .ReadyForShipment() .PlacedDaysAgo(2) .Build(); var ready2 = OrderBuilder.AnOrder() .ReadyForShipment() .PlacedDaysAgo(1) .Build(); var notReady = OrderBuilder.AnOrder() .Pending() // Not paid .Build(); var alreadyShipped = OrderBuilder.AnOrder() .ReadyForShipment() .Shipped() // Already shipped .Build(); SeedOrders(ready1, ready2, notReady, alreadyShipped); // Act var result = _repository.FindOrdersReadyForShipment(); // Assert Assert.Equal(2, result.Count()); Assert.All(result, o => Assert.True(o.Status == OrderStatus.Paid)); } [Fact] public void FindForCustomer_ReturnsOrdersInReverseChronologicalOrder() { // Arrange var customerId = new CustomerId(Guid.NewGuid()); var old = OrderBuilder.AnOrder() .ForCustomer(customerId) .PlacedDaysAgo(30) .Build(); var recent = OrderBuilder.AnOrder() .ForCustomer(customerId) .PlacedDaysAgo(1) .Build(); var newest = OrderBuilder.AnOrder() .ForCustomer(customerId) .PlacedOn(DateTime.UtcNow) .Build(); SeedOrders(old, recent, newest); // Act var result = _repository.FindForCustomer(customerId).ToList(); // Assert - newest first Assert.Equal(newest.Id, result[0].Id); Assert.Equal(recent.Id, result[1].Id); Assert.Equal(old.Id, result[2].Id); }}Builders should have sensible defaults—calling Build() with no customization should create a valid entity. Add convenience methods for common test states like ReadyForShipment(). Keep builders immutable (return this for chaining, but consider returning new instances for thread safety in parallel tests).
We've covered the full spectrum of repository testing. Let's consolidate the key principles:
Module Complete:
Congratulations! You've completed the Repository Pattern Deep Dive module. You now understand:
This knowledge equips you to build data access layers that are clean, maintainable, testable, and robust.
You've mastered the Repository pattern from concept to implementation to testing. Apply these patterns to build persistence layers that stand the test of time—and the test suite that ensures they work correctly.