C# Architecture Mastery — Testing Strategies in Clean Architecture (.NET) (Part 7)
Most teams say they “have tests”.
Very few have a testing strategy.
In Clean Architecture, testing is not an afterthought — it is a design outcome.
If your system is hard to test, your architecture is already telling you something is wrong.
In this Part 7, we’ll cover how testing aligns with Clean Architecture in .NET, what to test at each layer, and how senior teams avoid brittle test suites.
1. Clean Architecture Changes What You Test
Traditional systems test from the outside in.
Clean Architecture tests from the inside out.
Priority order:
- Domain logic
- Application use cases
- Infrastructure integrations
- Web / API layer
This ordering is intentional.
2. The Testing Pyramid (Clean Architecture Edition)
Classic testing pyramid:
- Unit tests (many)
- Integration tests (some)
- End‑to‑end tests (few)
Clean Architecture sharpens it:
Domain Tests ███████████
Application Tests ████████
Integration Tests ████
API / E2E Tests ██
Business rules deserve the most confidence.
3. Domain Tests — Pure and Fast
What to test
- Business rules
- Invariants
- Calculations
- State transitions
What NOT to test
- Databases
- HTTP
- Framework behavior
// ✅ Domain unit test
[Fact]
public void Order_Cannot_Be_Created_With_Negative_Total()
{
var act = () => new Order(-10);
act.Should().Throw<DomainException>();
}
If domain tests require mocks, something is wrong.
4. Application Layer Tests — Use Cases
What to test
- Orchestration logic
- Decision paths
- Interactions with abstractions
// ✅ Application test with mocks
[Fact]
public async Task CreateOrder_Saves_Order_And_Sends_Notification()
{
var repo = Substitute.For<IOrderRepository>();
var notifier = Substitute.For<IOrderNotifier>();
var useCase = new CreateOrderUseCase(repo, notifier);
await useCase.Execute(new CreateOrderCommand(100));
await repo.Received(1).SaveAsync(Arg.Any<Order>());
await notifier.Received(1).NotifyAsync(Arg.Any<Order>());
}
Mocks belong only at the boundaries.
5. Infrastructure Tests — Integration by Nature
Infrastructure tests answer:
“Does this thing actually work?”
Examples
- EF Core mappings
- SQL queries
- External APIs
- File systems
- Message brokers
// ✅ EF Core integration test
using var db = CreateRealDbContext();
var repo = new OrderRepository(db);
await repo.SaveAsync(order);
var saved = await db.Orders.FindAsync(order.Id);
saved.Should().NotBeNull();
These tests are:
- Slower
- Fewer
- Essential
6. API Tests — Thin and Focused
API tests should validate:
- Routing
- HTTP contracts
- Serialization
- Authentication / authorization
They should not re-test business rules.
// ✅ API test
var response = await client.PostAsJsonAsync("/orders", request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
If API tests are complex, controllers are probably fat.
7. What NOT to Test (Senior Rule)
Do not test:
- Framework internals
- EF Core itself
- ASP.NET Core routing logic
- Microsoft libraries
Test your code, not theirs.
8. Common Testing Smells
🚨 Warning signs:
- Mocking DbContext everywhere
- Integration tests replacing unit tests
- Tests coupled to HTTP models
- Tests breaking on refactors
- Massive test setup code
These usually indicate boundary violations.
9. The Real Goal of Testing
Testing is not about coverage.
It is about:
- Confidence
- Change safety
- Design feedback
Good tests make refactoring boring.
10. Senior-Level Testing Checklist
Before shipping, ask:
- Are business rules tested without infrastructure?
- Do use cases have focused unit tests?
- Are integrations tested against real systems?
- Are API tests thin?
- Do tests guide design decisions?
If yes, your architecture is working.
Final Thoughts
Clean Architecture does not guarantee testability.
Testability proves Clean Architecture.
If testing feels painful:
- Revisit boundaries
- Remove leaks
- Simplify responsibilities
Great tests are a side-effect of great architecture.
✍️ Written by Cristian Sifuentes — helping teams design systems that are easy to test, change, and trust.

Top comments (0)