---
name: test-organization-conventions
source: https://app.decimal.ai/s/test-organization-conventions@1/SKILL.md
source_sha256: 34e5da978fc1
---

# Test Organization Conventions

## Contract

Enforces a fixed, architecture-driven test directory taxonomy: the exact tier
names and order, the per-architecture top-level directories, the named test
categories, and fixture/BDD placement. Apply whenever laying out (or auditing)
a test tree so the architecture is inferable from the tree alone.

## Rules

### R1 — Four tiers, this exact order, test-type-first

Use exactly four tiers, named and ordered: `tests/unit/`,
`tests/integration/`, `tests/acceptance/`, `tests/e2e/`.

- `acceptance/` is a SEPARATE tier from `e2e/` — never collapse them.
- `acceptance/` runs through the application's driving ports against **in-memory
  adapters** and must be FAST (suitable for every CI run / pre-merge).
- `e2e/` runs the full stack through **real adapters** (real DB, HTTP, queues)
  and is slow.
- Hexagonal and Clean Architecture use test-type-first; they do **not** mirror
  the source rings/layers in the test tree.

### R2 — Per-architecture top-level directories (use these EXACT names)

| Architecture | Layout | Cross-boundary tests | Extra |
|---|---|---|---|
| Hexagonal / Clean | `tests/{unit,integration,acceptance,e2e}/` | — | type-first, not ring-mirrored |
| Layered (N-tier) | test tree MIRRORS source layer hierarchy | — | integration tests at layer boundaries |
| Modular monolith | `tests/modules/{module}/{unit,integration}/` | `tests/inter_module/` | one dependency-rule (architecture) test per module |
| Vertical slice | `features/{slice}/tests/` (co-located) | `tests/cross_feature/` | — |
| Microservices | `{service}/tests/{unit,integration,component,contract}/` | `e2e-tests/` (separate top-level project) | consumer-driven contracts |
| DDD (tactical) | `tests/{context}/domain/aggregates/` | `tests/bounded_context_integration/` | bounded-context-first |

- Modular monolith: the per-module dependency-rule (architecture) test enforces
  that the module does not import across its boundary.
- Microservices: cross-service E2E lives in a SEPARATE top-level project named
  `e2e-tests/`, never inside any one service's tree.
- DDD: organize bounded-context-first, then `domain/aggregates/` within.

### R3 — Named test categories (name them explicitly)

- **Event-driven**: add categories named **schema-contract** tests (event
  payload schema compatibility), **idempotency** tests (re-delivering the same
  event has no extra effect), and **saga-compensation** tests (compensating /
  rollback steps fire when a saga step fails). Keep them inside the test-type-
  first tree.
- **CQRS**: split **command** vs **query** inside each tier; add **projection**
  tests that cover BOTH projection rebuild from the event stream AND idempotency
  of re-applying events.
- **Hexagonal ports**: when several adapters implement one driven port, write
  ONE shared/abstract **port-contract** test suite and run it against EACH
  adapter implementation. Place it under `tests/integration/`.

### R4 — Microservices contract tests are consumer-driven

The consumer writes and owns the contract in its OWN repo; the provider verifies
that contract in the provider's OWN repo. Mocks alone drift from reality.

### R5 — BDD layout

- Gherkin `.feature` files under `tests/features/{domain}/`.
- Step implementations under `tests/step_defs/`.
- Organize step definitions BY DOMAIN CONCEPT (e.g. `order_steps.py`,
  `payment_steps.py`), NOT one step-def file per feature file. Shared steps then
  serve multiple features without duplication.

### R6 — Fixtures

- Session-scoped fixtures (DB engine, app instance) go in the ROOT
  `tests/conftest.py`.
- Every other `conftest.py` sits at the LOWEST directory where its fixtures
  apply (pytest discovers outermost→innermost, giving hierarchical scoping).
- Shared factories/builders go in a dedicated `tests/fixtures/` directory.
- Scope→placement: Session→root conftest; Module (schema, containers)→that
  directory's conftest; Function (cleanup/isolation)→`autouse=True` in the
  nearest conftest.

### R7 — Hybrid (recommended default)

Type-first for CI stages, feature-nested within:
`tests/unit/features/{domain}/`, `tests/integration/features/{domain}/`,
`tests/e2e/` for cross-cutting flows.

### R8 — Language file conventions

| Language | File pattern | Placement |
|---|---|---|
| Python (pytest) | `test_*.py` or `*_test.py` | separate `tests/` (recommended) |
| TS/JS (Jest) | `*.test.ts`, `*.spec.ts`, `__tests__/` | either |
| Java (JUnit/Maven) | `*Test.java` | `src/test/java` MIRRORS `src/main/java` package |
| Go | `*_test.go` | same directory (language-enforced co-location) |
| C# (xUnit/NUnit) | `*Tests.cs` | separate parallel test project |
| Rust | `#[cfg(test)] mod tests` + `tests/` | unit inline, integration in `tests/` |

## Worked examples (BEFORE = base default, AFTER = conforming)

### R1 — tiers

BEFORE: `tests/{unit, integration, e2e}/` (acceptance folded into e2e, all slow).
AFTER:
```
tests/
  unit/            # pure domain, fast
  integration/     # adapters vs real infra
  acceptance/      # driving ports + in-memory adapters, FAST
  e2e/             # full stack + real adapters, slow
```

### R2 — hexagonal vs source mirroring

BEFORE (mirrors the architecture rings):
```
tests/entities/  tests/use_cases/  tests/interface_adapters/  tests/frameworks/
```
AFTER (test-type-first):
```
tests/unit/  tests/integration/  tests/acceptance/  tests/e2e/
```

### R2 — modular monolith

BEFORE: `tests/orders/`, `tests/billing/` (flat, no tiers, no boundary test).
AFTER:
```
tests/modules/orders/unit/        tests/modules/orders/integration/
tests/modules/orders/test_dependencies.py   # dependency-rule test
tests/modules/billing/unit/       tests/modules/billing/integration/
tests/inter_module/test_orders_billing.py   # cross-module
```

### R2 — vertical slice

BEFORE: `tests/test_checkout.py` (tests pulled out of the slice).
AFTER:
```
features/checkout/tests/        # co-located with the slice
features/cart/tests/
tests/cross_feature/test_cart_to_checkout.py
```

### R2 — microservices

BEFORE: `payment-service/tests/e2e/test_full_purchase.py` (E2E inside one service).
AFTER:
```
payment-service/tests/{unit,integration,component,contract}/
e2e-tests/test_full_purchase.py   # SEPARATE top-level project
```

### R2 — DDD

BEFORE: `tests/test_order_aggregate.py` (flat, context invisible).
AFTER:
```
tests/ordering/domain/aggregates/test_order.py
tests/billing/domain/aggregates/test_invoice.py
tests/bounded_context_integration/test_ordering_billing.py
```

### R3 — event-driven categories

BEFORE: `tests/unit/` + `tests/integration/` only.
AFTER: add named categories `schema_contract/`, `idempotency/`,
`saga_compensation/` (e.g. a saga-compensation test asserts that when
`ReserveStock` succeeds but `ChargeCard` fails, the `ReleaseStock` compensation
fires).

### R3 — CQRS projection

BEFORE: `tests/test_read_model.py` asserts one query result.
AFTER: split `tests/unit/command/` vs `tests/unit/query/`; add
`tests/integration/projection/test_rebuild.py` (replays the event stream and
checks the projection) and `test_idempotent_apply.py` (applies the same events
twice, asserts the read model is unchanged).

### R3 — hexagonal port contract

BEFORE: `test_postgres_repo.py` and `test_inmemory_repo.py` each re-write the
same assertions independently.
AFTER:
```
tests/integration/test_repository_contract.py   # abstract suite for RepositoryPort
tests/integration/test_postgres_repository.py   # runs contract vs Postgres
tests/integration/test_inmemory_repository.py   # runs contract vs in-memory
```

### R4 — consumer-driven contracts

BEFORE: provider repo holds a mock of the consumer and tests against it.
AFTER: consumer repo defines & owns the contract (e.g. a Pact file); provider
repo runs provider-verification against that published contract.

### R5 — BDD step defs

BEFORE: `tests/step_defs/login_feature_steps.py`,
`tests/step_defs/checkout_feature_steps.py` (one file per feature).
AFTER:
```
tests/features/auth/login.feature
tests/features/order/place_order.feature
tests/step_defs/auth_steps.py        # by domain concept
tests/step_defs/order_steps.py
tests/step_defs/conftest.py
```

### R6 — fixtures

BEFORE: a `db_engine` session fixture duplicated inside each tier's conftest.
AFTER:
```
tests/conftest.py            # db_engine, app (session)
tests/integration/conftest.py# real-DB fixtures
tests/fixtures/factories.py  # shared builders
```

## Edge cases & exceptions

- **Layered is the one exception that mirrors source.** Every other style is
  organized by its primary architectural axis, but classic N-tier mirrors the
  source layer hierarchy; integration tests then verify layer-boundary contracts.
- **Java mirrors source too** by language convention (`src/test/java` parallels
  `src/main/java`) even under hexagonal — the package mirror is a language rule,
  not a violation of R1's "don't mirror rings."
- **Go forces co-location** (`*_test.go` beside the code); you cannot apply the
  separate-`tests/` layout for Go unit tests. Use Go's `package_test` external
  package for black-box tests; integration tests can still live in a `tests/` dir.
- **A single service that publishes events** combines R2 and R3: it keeps its
  own `{unit,integration,acceptance,e2e}` tiers AND adds event categories, while
  cross-service E2E still goes to the separate `e2e-tests/` project.
- **Acceptance vs e2e boundary**: if a test needs a real database to be
  meaningful it belongs in `integration/` or `e2e/`, not `acceptance/` —
  acceptance must stay on in-memory adapters to remain fast.
- **conftest depth**: a fixture used by only one tier must NOT live at the root;
  push it down to that tier's conftest so unrelated tiers don't pay for it.

## Do / Don't

- DON'T fold acceptance into e2e. DO keep four distinct tiers in order.
- DON'T mirror hexagonal/clean rings in the test tree. DO organize test-type-first.
- DON'T put cross-service E2E inside a service. DO use a separate `e2e-tests/` project.
- DON'T scatter cross-module tests into module dirs. DO use `tests/inter_module/`.
- DON'T bury cross-slice tests in one feature. DO use `tests/cross_feature/`.
- DON'T write per-adapter duplicate suites. DO write one port-contract suite run
  against each adapter, under `tests/integration/`.
- DON'T organize BDD steps per feature file. DO organize them by domain concept.
- DON'T duplicate session fixtures per tier. DO define them once in the root conftest.
- DON'T let the provider own the contract. DO make contracts consumer-driven.

## Common mistakes (base defaults)

- Emitting a generic `tests/{unit,integration,e2e}/` tree and omitting the
  `acceptance/` tier entirely.
- Mirroring source structure (rings/layers/packages) for hexagonal or clean,
  coupling tests to implementation.
- Inventing ad-hoc directory names (`tests/cross/`, `tests/shared/`) instead of
  the mandated `inter_module/`, `cross_feature/`, `bounded_context_integration/`.
- Forgetting the per-module dependency-rule test in a modular monolith.
- Treating event-driven like a plain CRUD service — no schema-contract,
  idempotency, or saga-compensation categories.
- For CQRS, testing only query output and skipping projection rebuild +
  idempotency.
- One step-def file per `.feature` file, causing duplicated shared steps.

## Quick checklist

- [ ] Four tiers present and ordered: unit, integration, acceptance, e2e.
- [ ] acceptance = in-memory/fast; e2e = real adapters/slow.
- [ ] Hexagonal/clean test-type-first, not ring-mirrored (layered mirrors source).
- [ ] Per-architecture dirs use the exact mandated names.
- [ ] Cross-boundary dirs: `inter_module/`, `cross_feature/`,
      `bounded_context_integration/`, separate `e2e-tests/`.
- [ ] Event-driven: schema-contract + idempotency + saga-compensation categories.
- [ ] CQRS: command/query split + projection (rebuild + idempotency).
- [ ] One port-contract suite per port, under `tests/integration/`.
- [ ] Contracts consumer-driven.
- [ ] BDD steps by domain concept; session fixtures in root conftest; shared
      builders in `tests/fixtures/`.
