Skip to content
Back to skills

Clean Architecture Testing

ASecurity

Use when it is unclear where a test belongs and what it is allowed to talk to — which test project and layer it goes in, whether it gets a real database, an in-memory fake, or no double at all, and whether a value object or a constructor is worth a test of its own. Also use when covering an adapter that calls an external API, a database or the file system, and when layer dependencies must be enforced by the build instead of by review.

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 26, 2026
ai-agentstypescriptpythongojavasqlnodespringtestinggitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned October 5, 2026

npx -y skills add SebastienDegodez/skraft-plugin --skill clean-architecture-testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Clean Architecture Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Clean Architecture Testing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/sebastiendegodez-clean-architecture-testing/badge)](https://www.skillsdirectory.com/skills/sebastiendegodez-clean-architecture-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: clean-architecture-testing
description: Use when it is unclear where a test belongs and what it is allowed to talk to — which test project and layer it goes in, whether it gets a real database, an in-memory fake, or no double at all, and whether a value object or a constructor is worth a test of its own. Also use when covering an adapter that calls an external API, a database or the file system, and when layer dependencies must be enforced by the build instead of by review.
---

# Clean Architecture Testing

## Overview

Defines **where** to test and **with which doubles** in a Clean Architecture solution. Language-agnostic. Does NOT redefine the TDD cycle — that is owned by `outside-in-tdd`.

**Core principle:** every test enters through **one boundary** and asserts at the next boundary. Choice of double follows the layer, not convenience.

**REQUIRED SUB-SKILL:** Use `outside-in-tdd` for the RED → SYNTHESIZE-GREEN → COMMIT cycle. This skill only routes you to the correct layer and doubles.

## The Iron Rule for Domain Tests

**Default = Application-level acceptance test.** Do NOT write Domain tests by default.

Extract a Domain test ONLY when a business rule:
- Has complex invariants or a large edge-case matrix, AND
- Is extracted into a reusable Policy / Domain Service / Specification.

**NEVER test directly:**
- Constructors (unless they enforce complex invariants)
- Simple value objects (covered by usage in Application acceptance tests)
- Getters / setters / DTOs / ViewModels / passive data structures

A test asserting `new PolicyNumber("abc").value == "abc"` is noise. It tests the compiler, not behavior.

## Test Project Organization

**Rule: exactly two test projects per bounded context**, regardless of language.

| Test project | Targets | Contains |
|---|---|---|
| `<Context>.UnitTest` | Domain + Application | Domain unit tests (rare, extracted rules) + Application acceptance tests (mocks on output gateways, in-memory fakes) |
| `<Context>.IntegrationTest` | Infrastructure + API + Architecture | Infrastructure integration tests (real containers), API end-to-end tests (in-process app host), architecture tests |

### Per-language mapping

| Ecosystem | `UnitTest` project | `IntegrationTest` project |
|---|---|---|
| .NET | `tests/<Context>.UnitTest/` csproj | `tests/<Context>.IntegrationTest/` csproj |
| Java (Maven / Gradle) | `<context>-unit-test` module | `<context>-integration-test` module |
| Python | `tests/unit/` package | `tests/integration/` package |
| TypeScript / Node | `test/unit/` workspace | `test/integration/` workspace |

### Multi-context solutions

One pair per bounded context. Shared helpers (fakes, builders) live in a separate non-test project (`TestKit` / `tests-shared`) referenced by both.

### Test folder naming

Test class folders and namespaces are named after the **feature or behavior** under test — never
after the GitHub issue, story ID, or sprint.

| Wrong | Right |
|---|---|
| `EligibilityCheck/STORY-42/` | `EligibilityCheck/` |
| `tests/OrderSubmission/issue-105/` | `tests/OrderSubmission/` |
| `PlaceOrder/Sprint3/` | `PlaceOrder/` |

One folder per feature or bounded behavior. The folder name answers "what does this test?"
not "which ticket created it?"

### Forbidden combinations

| Wrong | Why |
|---|---|
| Single test project mixing unit + integration | Slow tests pollute the fast loop; developers start skipping the suite |
| Splitting by production layer (`*.Domain.Test`, `*.Application.Test`, …) | Explodes project count, fights the boundary policy |
| End-to-end app host test in `UnitTest` | API tests need full composition — belong in `IntegrationTest` |
| Real container (DB / broker) in `UnitTest` | Breaks the <1 s suite promise |
| Architecture scanner test in `UnitTest` | Architecture tests are a CI gate, not a development loop |
| Third test project outside `UnitTest` / `IntegrationTest` | Creates a blind spot outside the two-speed suite; all tests belong to one of the two defined projects |

## Testing Strategy per Layer

| Layer | Test type | Entry point | Doubles | Target speed | When to write |
|---|---|---|---|---|---|
| **SharedKernel** | None | — | — | — | Never (interfaces + base classes, no logic) |
| **Domain** | Pure unit (rare) | Policy / Domain Service / Specification | None (real objects) | <10 ms | Only for extracted complex rules |
| **Application** | Acceptance (sociable) | Command / Query handler or UseCase | Mocks on output gateways, hand-written in-memory fakes for stateful reuse | <100 ms | **Default** — one per Gherkin scenario |
| **Infrastructure** | Integration | Gateway adapter (repository, read service, message handler) | Real I/O via containers (DB, broker); external APIs via contract mock server | 1-5 s | One per adapter contract |
| **API** | End-to-end | HTTP endpoint via in-process app host | Real internal stack; contract mock server for downstream externals | 1-5 s | Walking skeleton + one happy path per endpoint |
| **Architecture** | Static analysis | Assembly / module scan | None | <1 s | One rule per constraint, CI gate |

## Doubles Policy (by role, not by library)

| Role | Use for | Never for |
|---|---|---|
| **Real domain object** | Always, at every layer that touches Domain | — |
| **Mock / stub on output gateway** | Output gateways at Application level (repository, dispatcher, external service interfaces) | Domain objects, Application handlers |
| **Hand-written in-memory fake** | Reusable across many Application tests, stateful scenarios | One-off single-test cases |
| **Real container** (DB, broker) | Infrastructure adapter tests exclusively | Application or Domain tests |
| **Contract mock server** (for external APIs / brokers) | Infrastructure adapter tests against externals; API E2E with downstream externals | Internal domain logic, Application handlers |
| **In-process app host** (boots the full API) | API layer end-to-end tests exclusively | Application, Domain, Infrastructure |
| **Architecture scanner** | Architecture tests | Any behavioral test |

Pick one mocking library per solution and stick to it. Same for the app host factory. Mixing libraries across tests is a smell.

## Per-Layer Examples (pseudo-code)

Full runnable examples: [examples-dotnet.md](references/examples-dotnet.md) (.NET) and [examples-java.md](references/examples-java.md) (Java / Spring Boot, Maven). The roles are identical across languages.

### Application — acceptance test

```
given repository := mock of IOrderRepository
  and dispatcher := mock of IDomainEventDispatcher
  and handler := new PlaceOrderCommandHandler(repository, dispatcher)

when handler.handle(PlaceOrderCommand(id, "Alice"))

then repository received add(order where order.customerName == "Alice") exactly once
 and dispatcher received dispatch(events containing OrderPlacedEvent) exactly once
```

### Infrastructure — integration test

```
given db := start Postgres container
  and ctx := connect(db.connectionString) and migrate
  and sut := new OrderRepository(ctx)
  and order := Order.create(id, "Alice")

when sut.add(order)

then ctx.orders contains one row with customerName == "Alice"
```

### API — end-to-end test

```
given appHost := boot in-process app host
  and client := appHost.createHttpClient()

when response := client.POST("/orders", { customerName: "Alice" })

then response.status == 201
 and response.location is not null
```

### Architecture — layer rule

```
assert each project references only its inner neighbour
  (Api → Infrastructure → Application → Domain)

assert types in Domain depend only on Domain and the language core
assert types in Application depend only on Application, Domain and the language core
  else fail with "Violations: ..."
```

The language core excludes I/O, network and persistence. Allow-list the business code: a rule that only forbids the other layers lets every framework through.

## External Contract Mocks (Infrastructure & API)

When an adapter (Infrastructure) or endpoint (API) talks to an external HTTP / gRPC / async API:

- Start a **contract mock server** from the external's published contract (OpenAPI, gRPC proto, AsyncAPI).
- Point the adapter's client at the mock's URL / topic.
- In API end-to-end tests, wire downstream externals to the mock through DI overrides.
- The same contract file can later validate real responses (contract verification).

Out of scope for this skill — see `api-contract-testing` *(upcoming)* for the full pattern.

## Decision Tree — Which Test Layer?

```dot
digraph which_layer {
    node [shape=box];
    change [shape=diamond, label="What is the change?"];
    usecase [label="Use case orchestration\n(load/save/publish)"];
    gateway [label="Gateway adapter\n(DB, HTTP, broker)"];
    http [label="HTTP endpoint wiring"];
    rule [label="Complex reusable rule\n(many edge cases)"];
    boundary [label="Layer boundary rule"];
    app_test [style=filled, label="Application acceptance test\n(mocks on output gateways)"];
    infra_test [style=filled, label="Infrastructure integration test\n(real container / contract mock)"];
    api_test [style=filled, label="API end-to-end test\n(in-process app host)"];
    domain_test [style=filled, label="Domain unit test\n(pure)"];
    arch_test [style=filled, label="Architecture test\n(static scan)"];

    change -> usecase -> app_test;
    change -> gateway -> infra_test;
    change -> http -> api_test;
    change -> rule -> domain_test;
    change -> boundary -> arch_test;
}
```

Extended tree with tie-breakers: [doubles-decision-tree.md](references/doubles-decision-tree.md).

## Anti-Patterns

| Anti-pattern | Fix |
|---|---|
| Testing `new ValueObject(x).value == x` | Delete. Covered by usage in Application test. |
| Mocking a domain object | Use the real aggregate. Mock only output gateways. |
| Real container in an Application-level test | Move to Infrastructure layer or use an in-memory fake. |
| Shared global container across test classes | One container per test class, isolated lifecycle. |
| In-process app host used to test a handler | Switch to an Application-level test with mocks. |
| No architecture test guarding layer references | Add the rule. Layer discipline must be enforced by CI, not reviews. |
| Architecture test that only forbids the other layers | Allow-list what Domain and Application may depend on; a framework or an I/O client passes a layer-only rule. |
| In-memory fake DB provider used as Infrastructure test | Infrastructure tests MUST use the real provider via real container. In-memory providers silently accept invalid SQL. |
| One acceptance test covers 5 rules | Split: one Gherkin scenario = one test. |
| Duplicating acceptance coverage with a Domain test on the same rule | Delete the Domain test unless the rule was extracted. |

## Red Flags — STOP

These thoughts signal you're about to violate the policy:

- "I'll add a quick test on this constructor for safety."
- "Let me mock this aggregate to simplify the test."
- "I need an in-memory DB provider here because real containers are slow."
- "This handler test needs a real DB to verify my query."
- "Architecture tests slow down the build — I'll skip them."
- "A Domain test per value object will bump coverage."

**All of these mean: stop and re-read the table.** Coverage is not the goal. Behavior at the correct boundary is.

## References

- [examples-dotnet.md](references/examples-dotnet.md) — full runnable .NET examples (FakeItEasy, Testcontainers, in-process app host, architecture scanner)
- [examples-java.md](references/examples-java.md) — full runnable Java / Spring Boot examples (JUnit 5, Mockito, Testcontainers, ArchUnit)
- [architecture-rules.md](references/architecture-rules.md) — complete architecture rule set (project references + allow-list; .NET implementation)
- [doubles-decision-tree.md](references/doubles-decision-tree.md) — extended decision tree with tie-breakers

Files in this skill

  • SKILL.md11.9 KB
  • references/architecture-rules.md5.3 KB
  • references/doubles-decision-tree.md5 KB
  • references/examples-dotnet.md8.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.