What to Test — Coverage Strategy and Finding the Right Test Boundary

Test coverage strategy is about finding the tests that provide the most value for the least effort. Not everything needs a test — simple property assignments, trivial getters, and auto-generated code do not benefit from testing. High-value test targets are complex business rules, error paths that are hard to trigger manually, and critical security checks (ownership, authentication). Code coverage is a useful metric for finding gaps but should never be the primary goal.

BlogApp Testing Plan

// ── HIGH VALUE: PostsService business logic ────────────────────────────────

// 1. Post creation — happy path and slug uniqueness
[Fact]
public async Task CreateAsync_WhenSlugIsTaken_ThrowsConflictException()
{
    _repo.Setup(r => r.SlugExistsAsync("taken-slug", default)).ReturnsAsync(true);
    var request = new CreatePostRequest("Title", "taken-slug", "Body", "draft");

    Func<Task> act = () => _sut.CreateAsync(request, default);

    await act.Should().ThrowAsync<ConflictException>()
        .WithMessage("*taken-slug*");
}

// 2. Post ownership check for update/delete
[Fact]
public async Task IsAuthorOrAdminAsync_WhenUserIsOwner_ReturnsTrue()
{
    _repo.Setup(r => r.GetAuthorIdAsync(42, default)).ReturnsAsync("user-123");
    _user.Setup(u => u.UserId).Returns("user-123");
    _user.Setup(u => u.IsInRole("Admin")).Returns(false);

    var result = await _sut.IsAuthorOrAdminAsync(42, "user-123", default);

    result.Should().BeTrue();
}

[Fact]
public async Task IsAuthorOrAdminAsync_WhenUserIsAdmin_ReturnsTrueRegardlessOfOwnership()
{
    _repo.Setup(r => r.GetAuthorIdAsync(42, default)).ReturnsAsync("other-user");
    _user.Setup(u => u.IsInRole("Admin")).Returns(true);

    var result = await _sut.IsAuthorOrAdminAsync(42, "admin-user", default);

    result.Should().BeTrue();
}

// 3. JWT token claims
[Fact]
public void GenerateAccessToken_ContainsExpectedClaims()
{
    var user  = new ApplicationUser { Id = "u1", Email = "alice@example.com",
                                       DisplayName = "Alice" };
    var roles = new[] { "Author" };

    var token  = _tokenService.GenerateAccessToken(user, roles);
    var claims = _tokenService.ParseClaims(token);

    claims.Should().Contain(c => c.Type == "sub"   && c.Value == "u1");
    claims.Should().Contain(c => c.Type == "email" && c.Value == "alice@example.com");
    claims.Should().Contain(c => c.Type == "role"  && c.Value == "Author");
}

// ── LOW VALUE: skip these ──────────────────────────────────────────────────
// ❌ Testing EF Core DbContext setup (framework code)
// ❌ Testing that appsettings.json is bound correctly (configuration)
// ❌ Testing AutoMapper/Mapster mappings for simple field-to-field copies
// ❌ Testing that Console.WriteLine was called (trivial side effect)
// ❌ Testing that an empty list is returned when the database is empty
//    (unless there is specific behaviour expected — filtering, ordering)
//
// CODE COVERAGE TARGET: 75-85% line coverage on Services layer
//                        60-70% line coverage overall (lower due to infra code)
//
// Coverage command: dotnet test --collect:"XPlat Code Coverage"
//                  reportgenerator -reports:coverage.xml -targetdir:coverage-report
Note: Code coverage measures what percentage of code lines are executed during tests — not whether the tests make meaningful assertions. A test that calls every method but never asserts anything has 100% coverage and catches nothing. Conversely, 70% coverage with focused assertions on critical paths is more valuable than 100% coverage achieved by testing trivial getters. Use coverage reports to identify gaps (untested critical paths) rather than to set targets.
Tip: Write tests for every bug you fix — a regression test. When a bug is reported (“slug uniqueness isn’t checked when updating”), write a test that reproduces the bug (which will fail), then fix the bug (test passes). This ensures the bug cannot silently return in a future refactor. Regression tests are the highest-ROI tests — they prevent known bugs from recurring and document the fix for future developers.
Warning: Testing private methods is an anti-pattern. Private methods are implementation details — if the public behaviour is correct, the private methods are necessarily correct. If you feel the urge to test a private method, it usually means the class has too much responsibility and the logic should be extracted into a separate class with a public method that can be tested directly. Reflect on the design, not the test.

BlogApp Testing Plan Summary

Layer Test Count Framework Focus
PostsService 25 unit xUnit + Moq Business rules, error paths
AuthService / JWT 15 unit xUnit + Moq Token generation, claim extraction
SlugGenerator 10 unit xUnit [Theory] Input/output pairs
PostsController 20 integration WebApplicationFactory HTTP pipeline, status codes
AuthController 15 integration WebApplicationFactory Login, refresh, logout
Angular Services 30 unit Jasmine + SpyObj API calls, error handling
Angular Components 40 unit TestBed + Testing Library Template rendering, interactions
E2E Flows 20 E2E Cypress Critical user journeys

Common Mistakes

Mistake 1 — 100% coverage target leads to testing trivial code

❌ Wrong — testing every auto-property getter to reach 100%; coverage metric satisfied but no meaningful bug prevention.

✅ Correct — target 80% with focus on business logic; use coverage reports to find gaps in critical paths.

Mistake 2 — No tests for error paths (happy path only)

❌ Wrong — only testing successful operations; all error handling is untested and likely broken.

✅ Correct — at least one test per error path: NotFoundException, ConflictException, 401, 403, 412.

🧠 Test Yourself

A developer achieves 95% code coverage but discovers a production bug. How is this possible?