Module 3: Organizing The Suite Layers And Structure
8. Mini-project: reorganize a flat Reservo suite into layers
Overview
By the end of this lesson you will have put into practice, from start to finish, everything the module taught. The project is concrete: you are handed the flat Reservo suite —the twelve tests piled in a single directory, with one conftest.py that mixes the fixtures of the two layers— and you reorganize it into tests/unit/ and tests/integration/, each with its own conftest.py, moving each test to its layer and each fixture to the conftest.py that corresponds to it. You will run the two layers separately to see that the separation works, capture the --collect-only of the new tree to read it as documentation, and deliver a suite that runs clean and tells its own architecture through the shape. It is the "before" of lesson 2 turned into the "after" of the whole module, with your hands.
This matters because reorganizing an existing suite is one of the most frequent and least taught tasks of a project's real life. You almost never design the structure from scratch on an empty suite; you almost always inherit a flat bucket that grew without a plan —yours from months ago, or someone else's— and you have to give it shape without breaking what already works. The key, and what this project trains, is that reorganizing is an operation of shape, not of content: you move files and distribute fixtures, but you do not touch what each test asserts. The test count before and after must be identical. If you end up with more or fewer tests, or with a red, you changed two things at once and lost control. Reorganizing well is changing the box without touching what is inside.
Connection to the module: this lesson is the synthesis. Each step activates a previous lesson: separating the tests into two folders is the organization by layer (lesson 3) and the physical separation (lesson 5); distributing the fixtures among three conftest.py is the per-folder conftest.py (lesson 4); running each layer separately and reading the tree is discovery (lesson 6) and structure as documentation (lesson 7). And it closes the guide so far with a deliverable piece. Remember the boundary: here we categorize by folder; in module 4, the markers will add to this structure a second way of cutting the suite without moving a file.
The workshop you remodel without changing the tools
Think of it this way. You inherit the workshop of a carpenter who kept everything in a bucket: hammers, drill bits, screwdrivers, all jumbled. Your job is to set up the box with compartments —the one from lesson 2— so the workshop is navigable. But there is a golden rule in that remodel: you do not change or sharpen or repair any tool. The hammer that goes into the compartment is the same hammer that was in the bucket; it only changed place. If along the way you decided to sharpen the chisels, and something went wrong afterward, you would not know whether the problem was moving things or sharpening them. The remodel is one of organization, period: the same tools, now in compartments.
Reorganizing a suite is exactly that remodel. The twelve tests you inherit are the tools; the flat directory is the bucket; tests/unit/ and tests/integration/ are the compartments. Your job is to move each test to its compartment and distribute the fixtures to the correct conftest.py —nothing more—. You do not rename tests, do not merge asserts, do not "take the chance to improve": that would be sharpening the chisels in the middle of the move. The proof that you remodeled well and broke nothing is the simplest in the world: the same number of tests, all green, before and after. Twelve go into the bucket, twelve come out into the box.
The starting point: the flat suite
This is the suite you inherit. Twelve Reservo tests, all in a tests/ directory, with a single conftest.py. First, the flat conftest.py —and here is the underlying problem, besides the one of the folders—:
# tests/conftest.py — A SINGLE flat conftest: everything mixed, unit and integration
from datetime import datetime
import pytest
from reservo.calendar import Calendar
from reservo.models import Member, Room
from reservo.service import BookingService
@pytest.fixture
def focus():
return Room(id="focus", name="Focus", capacity=4, hourly_cents=2500)
@pytest.fixture
def ana():
return Member(id="ana", name="Ana", tier="basic")
@pytest.fixture
def bruno():
return Member(id="bruno", name="Bruno", tier="pro")
@pytest.fixture
def at():
def _at(hour):
return datetime(2026, 3, 10, hour)
return _at
# --- these two are ONLY used by the integration tests, but here the WHOLE suite sees them ---
@pytest.fixture
def calendar():
return Calendar()
@pytest.fixture
def service(calendar):
return BookingService(calendar)
Read it with the module's eyes. The root conftest.py has everything piled up: the light domain fixtures (focus, ana, bruno, at) next to the heavy integration ones (calendar, service). Since it is in the root, the whole suite sees all six —including the unit tests that will never use service—. It is the contamination of lesson 4: the pasta machine stored in the communal kitchen. And the twelve tests, all in a flat directory:
tests/
├── conftest.py
├── test_booking_flow.py # integration: assembles BookingService
├── test_cancel_flow.py # integration: assembles BookingService
├── test_overlaps.py # unit: pure overlaps()
├── test_pricing.py # unit: pure price_cents()
└── test_refund.py # unit: pure refund_cents()
Let us confirm the initial state: the fixture contamination and the starting green. First, what fixtures the whole suite sees:
python3 -m pytest tests --fixtures
What to expect (filtered to our fixtures). On my machine (Python 3.14.0, pytest 9.1.1):
focus -- tests/conftest.py:12
ana -- tests/conftest.py:17
bruno -- tests/conftest.py:22
at -- tests/conftest.py:27
calendar -- tests/conftest.py:35
service -- tests/conftest.py:40
The six fixtures, all in tests/conftest.py, all visible to all tests —the unit pricing ones see service even though they do not touch it—. And the starting green:
python3 -m pytest tests -q
............ [100%]
12 passed in 0.01s
Twelve green. This is the number to preserve: remodeling well means that in the end they are still twelve, green. We take it down noted.
The remodel, step by step
Reorganizing is a sequence of shape moves. None of them touch what a test asserts.
Step 1 — Classify each test by nature. Before moving anything, decide the layer of each file with the criterion of lesson 5 (one piece versus several). The three unit ones are test_overlaps.py, test_pricing.py, test_refund.py —each tests a pure function—. The two integration ones are test_booking_flow.py and test_cancel_flow.py —each assembles a BookingService—.
Step 2 — Create the two folders and move the files. tests/unit/ for the three pure ones, tests/integration/ for the two social ones:
tests/unit/test_overlaps.py
tests/unit/test_pricing.py
tests/unit/test_refund.py
tests/integration/test_booking_flow.py
tests/integration/test_cancel_flow.py
Step 3 — Distribute the fixtures among three conftest.py. Here you apply the building's rule (lesson 4): the shared goes down to the root, the specific goes to its layer. The domain fixtures (focus, ana, bruno, at) are used by both layers, so they stay in the root. The heavy ones (calendar, service) are used only by integration, so they go down to tests/integration/conftest.py. The root conftest.py is left clean:
# tests/conftest.py — ROOT, now only with the shared domain
from datetime import datetime
import pytest
from reservo.models import Member, Room
@pytest.fixture
def focus():
return Room(id="focus", name="Focus", capacity=4, hourly_cents=2500)
@pytest.fixture
def ana():
return Member(id="ana", name="Ana", tier="basic")
@pytest.fixture
def bruno():
return Member(id="bruno", name="Bruno", tier="pro")
@pytest.fixture
def at():
def _at(hour):
return datetime(2026, 3, 10, hour)
return _at
And the heavy ones move to their layer —notice that the root no longer imports Calendar or BookingService; those imports go with the fixtures—:
# tests/integration/conftest.py — the heavy fixtures, now in their layer
import pytest
from reservo.calendar import Calendar
from reservo.service import BookingService
@pytest.fixture
def calendar():
return Calendar()
@pytest.fixture
def service(calendar):
return BookingService(calendar)
Step 4 — Verify that the remodel did not change the content. Run the whole suite and confirm that they are still twelve, green. The shape changed; the content did not.
The resulting tree:
tests/
├── conftest.py # domain: focus, ana, bruno, at
├── unit/
│ ├── test_overlaps.py
│ ├── test_pricing.py
│ └── test_refund.py
└── integration/
├── conftest.py # heavy: calendar, service
├── test_booking_flow.py
└── test_cancel_flow.py
Worked example: the reorganized suite, running by layers
Now the proof that the remodel worked, and that the layers are really separated. First, the fast layer on its own:
python3 -m pytest tests/unit -v
What to expect:
collected 7 items
tests/unit/test_overlaps.py::test_touching_intervals_do_not_overlap PASSED [ 14%]
tests/unit/test_overlaps.py::test_nested_interval_overlaps PASSED [ 28%]
tests/unit/test_pricing.py::test_basic_member_pays_hourly_rate_times_hours PASSED [ 42%]
tests/unit/test_pricing.py::test_pro_member_gets_twenty_percent_off PASSED [ 57%]
tests/unit/test_refund.py::test_full_refund_at_72h PASSED [ 71%]
tests/unit/test_refund.py::test_half_refund_at_36h PASSED [ 85%]
tests/unit/test_refund.py::test_no_refund_at_12h PASSED [100%]
============================== 7 passed in 0.01s ===============================
Seven unit tests, alone. Now the integration layer on its own:
python3 -m pytest tests/integration -v
What to expect:
collected 5 items
tests/integration/test_booking_flow.py::test_booking_a_room_charges_the_pro_price PASSED [ 20%]
tests/integration/test_booking_flow.py::test_room_is_unavailable_after_it_is_booked PASSED [ 40%]
tests/integration/test_booking_flow.py::test_double_booking_the_same_slot_is_rejected PASSED [ 60%]
tests/integration/test_cancel_flow.py::test_cancelling_72h_ahead_refunds_in_full PASSED [ 80%]
tests/integration/test_cancel_flow.py::test_cancelling_frees_the_room PASSED [100%]
============================== 5 passed in 0.01s ===============================
Five integration ones, alone. Seven plus five are twelve: the same twelve of the flat bucket, not one more or one less. The remodel preserved the content (twelve tests, all green) and gained the shape (two layers runnable separately). That is reorganizing well.
And the proof that the fixtures were distributed and no longer contaminate: --fixtures in the unit layer no longer shows service.
python3 -m pytest tests/unit --fixtures
What to expect (filtered to our fixtures):
focus -- tests/conftest.py:10
ana -- tests/conftest.py:15
bruno -- tests/conftest.py:20
at -- tests/conftest.py:25
Four fixtures, all from the domain, all from the root. calendar and service disappeared from the unit layer's horizon —they now live in tests/integration/conftest.py, out of its reach—. The communal kitchen is left clean; the heavy machine is on its floor. Compared with the --fixtures from the start, which showed all six for the whole suite, this is the end of the contamination.
Read the new tree as documentation
The last deliverable is the board. Capture the tree of the reorganized suite:
python3 -m pytest --collect-only
What to expect:
collected 12 items
<Dir tests>
<Dir integration>
<Module test_booking_flow.py>
<Function test_booking_a_room_charges_the_pro_price>
<Function test_room_is_unavailable_after_it_is_booked>
<Function test_double_booking_the_same_slot_is_rejected>
<Module test_cancel_flow.py>
<Function test_cancelling_72h_ahead_refunds_in_full>
<Function test_cancelling_frees_the_room>
<Dir unit>
<Module test_overlaps.py>
<Function test_touching_intervals_do_not_overlap>
<Function test_nested_interval_overlaps>
<Module test_pricing.py>
<Function test_basic_member_pays_hourly_rate_times_hours>
<Function test_pro_member_gets_twenty_percent_off>
<Module test_refund.py>
<Function test_full_refund_at_72h>
<Function test_half_refund_at_36h>
<Function test_no_refund_at_12h>
This tree is your final deliverable, and it reads itself (lesson 7): two layers, a core of pure logic (overlap, pricing, refund) under unit/, two complete flows (booking, cancelling) under integration/. The same --collect-only that at the start of the module was a flat list of five modules in a row is now a map. Without having touched what a single test asserts, you turned the bucket into the box with compartments.
Your delivery
The project asks you for three things, and each one exercises something different:
- The reorganized suite. Take the flat Reservo suite (the one of this lesson, or one you assemble by piling the twelve tests in a directory with a
conftest.pythat mixes the fixtures) and reorganize it intotests/unit/andtests/integration/, with the root for the domain fixtures andtests/integration/conftest.pyforcalendarandservice. It must run green by layers:pytest tests/unit(7) andpytest tests/integration(5). - The proof that the content did not change. Run
pytest tests(or plainpytest) before and after the remodel and capture the two counts:12 passedat the start,12 passedat the end. That identical number is the demonstration that you reorganized the shape without touching the content —you moved tools, did not sharpen them—. - The tree as documentation and the end of contamination. Capture the
--collect-onlyof the reorganized tree (that reads as the system's map) and the--fixturesoftests/unitshowing that it no longer seescalendarorservice. Those two are the evidence that you gained both things of the module: a shape that documents and layers that do not contaminate.
An honest "done" criterion, in the spirit of this guide: the remodel is ready not when the suite passes —the flat bucket also passed—, but when it passes by separate layers (7 and 5, not just 12 together), when the --fixtures of the unit layer no longer drags the integration machinery, and when a coworker who did not see the code can read the --collect-only and tell you the architecture of Reservo. If those three things hold, you set up the box with compartments.
Common mistakes
Changing the content "along the way" during the remodel (of scope). What happens: when moving the files, someone takes the chance to rename a test, merge two, or "fix" an assert. Why it happens: having the files open invites improving everything at once. How to detect it: if the count before and after is not identical (12 → 12), or if a red appears, you changed content and shape at once and no longer know which caused what. How to fix it: reorganize in one step —only moving files and distributing fixtures— and verify 12 passed → 12 passed. Improving the tests is another task, another moment, another commit. One thing at a time.
Forgetting to lower the heavy fixtures to their layer (of incomplete remodel). What happens: someone moves the test files to unit/ and integration/ but leaves all the fixtures in the root conftest.py. Why it happens: moving files feels like "done", and distributing fixtures is the least visible step. How to detect it: run pytest tests/unit --fixtures; if you still see calendar and service, the remodel was left half done —the folders were separated but the contamination continues—. How to fix it: move calendar and service (and their import of Calendar/BookingService) to tests/integration/conftest.py. The reorganization is not only of test files: the fixtures also travel to their layer, or the isolation is only apparent.
Classifying a test by its name or its size instead of its nature (of criterion). What happens: someone puts test_booking_flow.py in unit/ "because it is short" or test_pricing.py in integration/ "because pricing is central". Why it happens: size or importance gets confused with layer. How to detect it: look at what the test assembles —if it sets up a Calendar/BookingService, it is integration; if it calls a pure function, it is unit— no matter how many lines it has or how important the topic is. How to fix it: apply the criterion of lesson 5 (one piece versus several). A test that imports BookingService is not unit even if it fits in five lines; misplacing it would make the structure lie (lesson 7).
Exercises
Exercise 1 — Distribute these fixtures. Besides the six of the example, the flat suite has three more fixtures in its root conftest.py. For each one, say to which conftest.py you move it in the remodel and why. (a) boardroom, a large Room used by pricing tests (unit) and flow tests (integration). (b) booked_calendar, a Calendar with a booking already loaded, used only by integration tests. (c) three_hours, the constant 3 used only by the unit pricing tests.
See solution
- (a)
boardroom→ stays in the root (tests/conftest.py). Both layers use it, so it is shared domain, likefocusandana. It goes down to the communal kitchen so both inherit it without duplicating it. - (b)
booked_calendar→tests/integration/conftest.py. It assembles aCalendarwith state and only integration tests use it: it is heavy machinery of a single layer. It moves withcalendarandservice. - (c)
three_hours→tests/unit/conftest.py. Only unit pricing tests use it; it is specific to that layer. Iftests/unit/conftest.pydoes not yet exist, the remodel creates it to host it (it is thehours_3of the previous lessons). It makes no sense in the root or in integration.
The rule you applied: "who uses it" decides the height. All layers → root; only integration → its conftest; only unit → the unit conftest. Distributing the fixtures well is the least visible, and most important, half of the remodel.
Exercise 2 — Verify without running. A coworker says they finished the remodel. Before running anything, what three commands would you ask them to execute to demonstrate they reorganized well, and what would you expect to see in each? Explain what each result would prove.
See solution
Three commands, each proves a different thing:
pytest tests -q(or plainpytest) → I would expect12 passed. It proves the content did not change: they are still the twelve tests of the flat bucket, all green. If it showed another number or some red, the remodel touched content, not only shape.pytest tests/unit -qandpytest tests/integration -q→ I would expect7 passedand5 passed. It proves the layers are really separated and runnable on their own: 7 + 5 = 12, the subsets are disjoint and add up to the total. Ifpytest tests/unitgave 8, some integration test was misplaced inunit/.pytest tests/unit --fixtures→ I would expect to seefocus,ana,bruno,atbut notcalendarorservice. It proves the fixtures were distributed and the contamination ended: the unit layer no longer sees the integration machinery. If it still showedservice, the heavy fixtures are still in the root and the remodel was left half done.
The three together cover the two dimensions of a good remodel: content preserved (command 1) and shape gained, both in folders (command 2) and in fixtures (command 3). An optional fourth, pytest --collect-only, confirms that the tree reads as documentation.
Exercise 3 — Close the module: write the specification from the tree. Run pytest --collect-only on your reorganized suite and, using only the shape of the tree and the names of the tests, write the specification of Reservo that the suite documents: how many layers, what pure rules, what flows, and one business rule per test that you can read from its name. Then reflect: what did this reorganization give you that the flat bucket of lesson 2 did not?
See solution
From the reorganized tree, read without opening code, comes this specification:
Architecture: two test layers —unit (pure logic) and integration (complete flows)—.
Pure rules (under unit/):
- Schedule overlap (
test_overlaps): two intervals that touch do not overlap; an interval contained in another does. - Pricing (
test_pricing): the basic member pays the hourly rate times the hours; the pro gets 20% off. - Refund (
test_refund): at 72 h from the start the refund is full; at 36 h, half; at 12 h, nothing.
Complete flows (under integration/):
- Booking (
test_booking_flow): booking charges the correct pro price; after booking, the room becomes unavailable; a double booking of the same slot is rejected. - Cancelling (
test_cancel_flow): cancelling 72 h ahead refunds the full amount; cancelling frees the room.
That is the living documentation of Reservo, extracted from the pure tree.
What the reorganization gave you that the flat bucket did not: the three things of the module. Being able to run by layers —pytest tests/unit as a fast net during development, pytest tests/integration when you want to verify the flows—, which in the bucket required naming files by hand. Fixture isolation —the unit layer no longer loads calendar/service—, which in the bucket contaminated everyone. And a shape that documents —the tree you just read as a specification—, which in the bucket was a flat list without architecture. All that without changing what a single test asserts: the same twelve, now in a structure that can be navigated, run in parts and understood at a glance. That is the leap from "a pile of tests that passes" to "a suite with architecture", which was the topic of the whole module.
Summary and next step
In this mini-project you gathered the whole module into a real task: reorganize the flat Reservo suite into layers. You started from the bucket —twelve tests in a directory, with a conftest.py that piled the light domain fixtures next to the heavy integration ones, contaminating the whole suite— and you remodeled it in four steps of pure shape: you classified each test by nature, moved the files to tests/unit/ and tests/integration/, distributed the fixtures (the domain in the root, calendar/service in the integration layer), and verified that the content did not change. You verified it by running: 12 passed before and after (content preserved), pytest tests/unit gives 7 and pytest tests/integration gives 5 (layers runnable separately), the --fixtures of the unit layer no longer shows calendar or service (contamination ended), and the --collect-only draws a tree that reads as the specification of Reservo (shape that documents). You moved the tools without sharpening them: the bucket became the box with compartments.
With this you know how to design and apply the physical structure of a suite: organize by layer or by feature, a per-folder conftest.py that isolates, separate unit from integration to run them on their own, understand the discovery that makes everything possible, and read —and keep honest— the structure as documentation. You no longer confuse "the suite passes" with "the suite has architecture".
What comes next in the guide is module 4: markers and configuration. So far you categorized the tests by their physical place —the folder where they live—. The markers (@pytest.mark.slow, @pytest.mark.integration) will give you a second way of categorizing, orthogonal to the folders: a label you can stick on any test, wherever it is, to then select subsets with -m that cut the suite in a direction the folder structure cannot —for example, "all the slow tests, no matter what layer or feature they live in"—. You will see how to register markers in pyproject.toml, how the configuration becomes the framework's contract, and how folders and markers complement each other: the physical structure you built in this module, plus a layer of labels that crosses it. The foundations of organization you already have; what comes is the second dimension.
Resources
- pytest — Good Integration Practices — the official project-structure conventions; the standard toward which you remodeled the flat Reservo bucket.
- pytest — conftest.py: sharing fixtures across files — the reference on how to distribute fixtures across several
conftest.py; the basis of step 3 of the remodel. - pytest — How to invoke pytest (
--collect-only) — how to run layers separately and draw the tree; the commands with which you verified and documented the reorganized suite.