Testing with Repository Abstractions — In-Memory and Mocking

Repository abstractions exist specifically to enable testing without a real database. A service that depends on IPostRepository can be tested with a mock (Mock<IPostRepository>) that returns predefined data — making tests instant, deterministic, and free of database setup. Deeper integration tests can use EF Core’s InMemory provider or a real LocalDB database. Understanding which test approach is appropriate for which scenario, and where each sits in the test pyramid, prevents the common mistake of over-mocking (never testing the real SQL) or under-mocking (slow integration tests for simple business logic).

Unit Testing with Moq

// ── Unit test for PostService using mock IPostRepository ──────────────────
public class PostServiceTests
{
    private readonly Mock<IPostRepository> _mockRepo;
    private readonly PostService           _sut;

    public PostServiceTests()
    {
        _mockRepo = new Mock<IPostRepository>();
        _sut      = new PostService(_mockRepo.Object,
            Mock.Of<ILogger<PostService>>());
    }

    [Fact]
    public async Task GetByIdAsync_ExistingPost_ReturnsDto()
    {
        // Arrange
        var post = Post.Create("Test Title", "test-title", "A".PadRight(50), "user-1");
        _mockRepo.Setup(r => r.GetByIdAsync(42, It.IsAny<CancellationToken>()))
                 .ReturnsAsync(post);

        // Act
        var result = await _sut.GetByIdAsync(42, CancellationToken.None);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Test Title", result.Title);
        _mockRepo.Verify(r => r.GetByIdAsync(42, It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task CreateAsync_DuplicateSlug_ThrowsConflictException()
    {
        // Arrange
        _mockRepo.Setup(r => r.SlugExistsAsync("taken-slug", It.IsAny<CancellationToken>()))
                 .ReturnsAsync(true);

        var request = new CreatePostRequest
        {
            Title = "Test", Body = "A".PadRight(50), Slug = "taken-slug",
        };

        // Act & Assert
        await Assert.ThrowsAsync<ConflictException>(
            () => _sut.CreateAsync(request, "user-1", CancellationToken.None));

        // Verify AddAsync was never called (exception before it)
        _mockRepo.Verify(r => r.AddAsync(It.IsAny<Post>(), It.IsAny<CancellationToken>()),
            Times.Never);
    }
}
Note: Unit tests with Moq test service business logic — not the database query logic inside repositories. They verify: does the service throw when a slug exists? Does it call AddAsync when validation passes? Does it map entity to DTO correctly? The SQL translation, index usage, and actual database interactions are verified in integration tests using a real database. Each test layer has a distinct responsibility — do not blur them by putting business logic assertions in integration tests or database query assertions in unit tests.
Tip: Create a base test class with common setup to avoid boilerplate in each test: public abstract class ServiceTestBase { protected Mock<IUnitOfWork> MockUow = new(); protected Mock<ILogger<T>> MockLogger<T>() => Mock.Of<ILogger<T>>(); }. Test classes inherit from this and override specific mock setups. Also consider using AutoFixture or Bogus for generating test data — var post = _fixture.Create<Post>() is faster than writing object construction code for every test.
Warning: EF Core’s InMemory provider does not enforce referential integrity, unique constraints, or most database-level validations. A test that passes with InMemory may fail on a real database because a unique constraint is violated. InMemory is useful for lightweight tests of LINQ query logic, but for testing migrations, constraints, and actual SQL generation, use a real SQL Server (LocalDB) or SQL Server in a Docker container via TestContainers.

Integration Tests with InMemory Provider

// ── EF Core InMemory integration test ─────────────────────────────────────
public class PostRepositoryTests : IDisposable
{
    private readonly AppDbContext  _db;
    private readonly PostRepository _sut;

    public PostRepositoryTests()
    {
        var opts = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())   // unique DB per test
            .Options;
        _db  = new AppDbContext(opts);
        _sut = new PostRepository(_db);
    }

    [Fact]
    public async Task AddAsync_ValidPost_AssignsId()
    {
        var post = Post.Create("Title", "slug", "Body text here and more.", "user-1");
        var added = await _sut.AddAsync(post);
        Assert.True(added.Id > 0);
    }

    [Fact]
    public async Task SlugExistsAsync_ExistingSlug_ReturnsTrue()
    {
        var post = Post.Create("Title", "my-slug", "Body text.", "user-1");
        await _sut.AddAsync(post);
        Assert.True(await _sut.SlugExistsAsync("my-slug"));
    }

    public void Dispose() => _db.Dispose();
}

Common Mistakes

Mistake 1 — Reusing the same InMemory database across tests (test pollution)

❌ Wrong — tests share state; data from test A affects test B results.

✅ Correct — use a unique database name per test: Guid.NewGuid().ToString() for the database name.

Mistake 2 — Trusting InMemory tests for constraint validation (no constraints enforced)

❌ Wrong — InMemory test passes with duplicate slug; real database throws unique constraint violation.

✅ Correct — test constraint enforcement with real SQL Server (LocalDB or Docker); use InMemory only for query logic.

🧠 Test Yourself

A unit test for PostService.CreateAsync() uses Mock<IPostRepository>. What specifically does this test verify?