C# Architecture Mastery — Testing Strategies in Clean Architecture (.NET) (Part 7)
Source: Dev.to
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.
Classic testing pyramid
- Unit tests (many)
- Integration tests (some)
- End‑to‑end tests (few)
Clean Architecture sharpens it
| Layer | Coverage |
|---|---|
| Domain Tests | ███████████ |
| Application Tests | ████████ |
| Integration Tests | ████ |
| API / E2E Tests | ██ |
Business rules deserve the most confidence.
Business rules
- Invariants
- Calculations
- State transitions
Domain unit test
// ✅ Domain unit test
[Fact]
public void Order_Cannot_Be_Created_With_Negative_Total()
{
var act = () => new Order(-10);
act.Should().Throw();
}
If domain tests require mocks, something is wrong.
Application test with mocks
// ✅ Application test with mocks
[Fact]
public async Task CreateOrder_Saves_Order_And_Sends_Notification()
{
var repo = Substitute.For<IOrderRepository>();
var notifier = Substitute.For<INotifier>();
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<Notification>());
}
Mocks belong only at the boundaries.
Infrastructure tests
Infrastructure tests answer: “Does this thing actually work?”
- EF Core mappings
- SQL queries
- External APIs
- File systems
- Message brokers
EF Core integration test
// ✅ 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, but essential.
API tests
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.
Do not test
- Framework internals
- EF Core itself
- ASP.NET Core routing logic
- Microsoft libraries
Test your code, not theirs.
Warning signs
- Mocking
DbContexteverywhere - Integration tests replacing unit tests
- Tests coupled to HTTP models
- Tests breaking on refactors
- Massive test setup code
These usually indicate boundary violations.
Testing is not about coverage
It is about:
- Confidence
- Change safety
- Design feedback
Good tests make refactoring boring.
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.
If testing feels painful
- Revisit boundaries
- Remove leaks
- Simplify responsibilities
Great tests are a side‑effect of great architecture.