Purpose¶
In a hexagonal-architecture project the tests tree is expected to shadow the src tree: a test for src/contexts/orders/domain/entities/order.py lives at tests/contexts/orders/domain/entities/test_order.py. This rule enforces that every directory under tests has a counterpart under src, so the test suite can never invent a layout of its own or leave a stale directory behind after the source it covered was moved.
Configuration¶
rules:
- name: tests-mirror-src
type: mirror
description: The tests tree mirrors the src tree
source: src
mirror: tests
direction: mirror-to-source
mirror_extra_allowed: [doubles, fixtures, conftest]
source and mirror name the two trees. direction: mirror-to-source requires every directory under tests to have a matching directory under src — the source drives the layout, and the tests tree adds no structure the source does not already have.
The direction runs this way on purpose: a test's location is determined by the source module it covers, so the mirror side must not diverge. The reverse is not required — not every source directory needs a test, so a source module without a test is fine. mirror_extra_allowed exempts test-only scaffolding directories (doubles, fixtures, conftest) from needing a source counterpart.
Both examples below share this source tree:
src/
└── contexts/
└── orders/
└── domain/
├── entities/
│ └── order.py
└── errors/
└── order_not_found_error.py
Violation Example¶
The tests tree mirrors entities and errors, but adds a services directory the source tree never defined — an orphan with no source counterpart:
tests/
└── contexts/
└── orders/
└── domain/
├── entities/
│ └── test_order.py
├── errors/
│ └── test_order_not_found_error.py
└── services/ # no src/contexts/orders/domain/services
└── test_pricing.py
Passing Example¶
Drop the orphan services directory. A test-only doubles directory may stay even though src has no such directory, because doubles is listed in mirror_extra_allowed:
tests/
└── contexts/
└── orders/
└── domain/
├── doubles/ # exempt via mirror_extra_allowed
│ └── fake_order_repository.py
├── entities/
│ └── test_order.py
└── errors/
└── test_order_not_found_error.py
Output¶
$ psl check
tests/contexts/orders/domain/services
[tests-mirror-src] The tests tree mirrors the src tree
no source counterpart 'src/contexts/orders/domain/services'
Found 1 violation(s).