The 14 rules of CANDO-TEST
Practical constraints for preserving the framework’s boundaries, dependency direction, and readability as a test suite grows.

I'm an enthusiastic Chilean software engineer in New Zeland. I mostly focus on the back-end of the systems. This is my site, Señor Developer, where I share my knowledge and experience.
Architecture rarely collapses because a team makes one obviously disastrous decision. More often, it erodes through small exceptions that each seem reasonable: a selector placed directly in a test, a Page Object that constructs another Page Object, a convenient data object copied into a second scenario.
The test suite still runs. The structure still looks familiar. But the boundaries no longer carry much weight.
The rules of CANDO-TEST are intended to make that erosion visible. They turn the framework’s central idea—each layer owns one concern, and dependencies move downwards—into decisions that can be applied during implementation and review.
This is the practical companion to CANDO-TEST: A clearer architecture for UI test automation. That article explains the layers and the thinking behind them. Here, I’ll focus on the constraints that keep the architecture intact as a suite grows.
1. Keep one domain in one folder
A domain is a cohesive, user-facing feature whose tests and supporting code tend to change together. All of its Assessments, Components, Fixtures, Network Mocks, Data Variant Creators, and Page Objects should live together.
src/
auth/
assessments/
components/
dataVariants/
fixtures/
networkMocks/
objects/
The boundary should follow the feature, not necessarily a route. One domain may span several pages, while a dashboard route may contain several independent domains.
Why it matters: Organising by technical layer scatters the knowledge required to understand a feature. Domain locality makes that knowledge easier to find, change, and remove.
Do: Put the code for an authentication flow under src/auth/.
Don’t: Distribute it across top-level assessments/, pages/, and components/ directories.
2. Use Fixtures as the composition root
Fixtures should construct and provide the collaborators an Assessment needs: Page Objects, Network Mocks, and Data Variant Creators. The domain fixture is the single place where these dependencies are assembled.
This is an application of Inversion of Control. Assessments receive ready-to-use collaborators instead of knowing how to build them.
Why it matters: Manual construction spreads lifecycle and infrastructure decisions across test files. Fixtures keep those decisions in one inspectable place and allow the test runner to handle setup and cleanup correctly.
Do: Receive todoPage and todoApiNetworkMock from the domain fixture.
Don’t: Construct them in an Assessment or use beforeEach as a dependency-injection mechanism.
3. Import the test entry point from the domain fixture
An Assessment should import test and any re-exported assertion support from its domain fixture, not directly from the underlying automation framework.
import { test } from "../fixtures/todo.fixtures";
This small import rule makes the composition root difficult to bypass accidentally.
Why it matters: A direct framework import gives an Assessment access to low-level runtime primitives and avoids the domain fixture. The test may still pass, but the architecture is no longer protecting it.
Do: Import from the fixture belonging to the Assessment’s domain.
Don’t: Import the base test or expect directly from Playwright or an equivalent framework.
4. Put cross-cutting setup in a shared session fixture
Authentication, base URLs, common headers, cache resets, and similar concerns usually apply across several domains. They belong in a shared session fixture that each domain fixture extends.
base test
↓
shared session fixture
↓
domain fixture
Domain fixtures should add only what is specific to their feature.
Why it matters: Reimplementing session concerns in every domain creates drift. A change to authentication or cache handling then requires several coordinated edits.
Do: Define authentication once and inherit it through the fixture chain.
Don’t: Repeat the same session setup inside every domain.
5. Compose Components from shallow shared bases
A domain Component declares what its locators are and how its particular fields map to data. A shared base Component can own a reusable interaction protocol, such as submitting a form or asserting a field error.
Shared bases should remain shallow. If behaviour becomes richer, composition is usually easier to follow than another level of inheritance. Domain Components may also compose stable subregions of the same interface, provided they remain focused on element-level behaviour.
New shared bases should be extracted when two or more domains need the same protocol. They shouldn’t be created in anticipation of reuse that may never appear.
Why it matters: Shared protocols prevent duplication, but deep or speculative inheritance hides behaviour across several classes. The aim is reuse without recreating the coupling problems the layers were meant to solve.
Do: Let CaseFormComponent extend a proven BaseFormComponent.
Don’t: Reimplement the same submit-and-validate protocol in every domain, or build a deep hierarchy of hypothetical variants.
6. Let Assessments drive behaviour, not structure
An Assessment describes what should happen and under which conditions. It interacts with domain-level collaborators exposed by the fixture: Page Objects, Network Mocks, and Data Variant Creators.
It shouldn’t know about Components, locators, browser handles, request contexts, or the raw assertion API. Its configuration phase declares data and network conditions; its execution phase expresses a user action and the observable result.
test("shows a success confirmation", async ({
todoPage,
todoApiNetworkMock,
todoDataVariantCreator,
}) => {
const data = todoDataVariantCreator.createValidData();
await todoApiNetworkMock.withSuccessFetchResponse().mock();
await todoPage.open();
await todoPage.createTodo(data);
await todoPage.assertCreatedSuccessfully();
});
Why it matters: An Assessment that reaches into page structure becomes coupled to implementation details. When the interface changes, the scenario changes even if the behaviour does not.
Do: Call todoPage.createTodo(data).
Don’t: Locate the form fields or destructure the underlying browser page inside the Assessment.
7. Make Page Objects speak user language
Page Objects own page-level orchestration: navigation, URL state, modal and toast coordination, cross-Component sequencing, and full-page transitions. Their methods should describe what a user is trying to do.
Components own element-level interactions. A Page Object can coordinate several Components, but it shouldn’t contain their locators or reimplement their behaviour.
Page Objects also shouldn’t compose other Page Objects. A method may trigger navigation, but it shouldn’t return or own the destination Page Object. Each page can be constructed independently by the fixture.
Why it matters: User-language methods keep scenarios readable and insulate them from structural changes. Preventing nested Page Object graphs also avoids coupling the suite to its navigation structure.
Do: Expose createTodo(data) or navigateToSettings().
Don’t: Expose clickSubmitButton() or return a SettingsPage from a dashboard method.
8. Reuse before rewriting
Before adding another helper, Component, data factory, or Network Mock, look for an existing abstraction that owns the same responsibility. Extend or configure it when doing so preserves a clear boundary.
Reuse isn’t the same as forcing unrelated concepts together. The existing abstraction should genuinely represent the same protocol or domain idea.
Why it matters: Meaningful reuse narrows the blast radius of change. A selector, data shape, or network response can be updated in one place rather than several near-duplicates.
Do: Add a named scenario to an existing Data Variant Creator.
Don’t: Copy an existing factory into a second Assessment because changing it locally feels quicker.
9. Don’t overbuild UI Components
Not every visible element deserves a class. A Component should exist when a region has interaction logic worth encapsulating, is reused, or forms a natural domain seam.
A multi-step checkout form is a reasonable Component. A wrapper around one static heading is probably not.
Why it matters: Excessively granular Components add names, files, constructors, and navigation without isolating meaningful change. The abstraction costs more than it saves.
Do: Group related controls into a Component that represents a coherent part of the interface.
Don’t: Create a class for every button, label, or link to make the structure appear more modular.
10. Generate test data through named variants
All scenario data should originate in a Data Variant Creator. Each method names an intent:
const valid = todoDataVariantCreator.createValidData();
const overdue = todoDataVariantCreator.createWithPastDueDate();
const urgent = todoDataVariantCreator.createValidData({
priority: "high",
});
A scoped override can pin one dimension of a recognised scenario. It shouldn’t replace the scenario with an arbitrary object built inside the Assessment.
Data Variant Creators are pure factories. They don’t call APIs, mutate application state, or use test-framework assertions. They also define their own types inside the test repository instead of importing types or schemas from the application under test.
Why it matters: Named variants explain why the data exists. Repository-owned types also keep the tests independent: they assert the application’s observable contract rather than its consistency with its own internal definitions.
Do: Use createValidData({ priority: "high" }).
Don’t: Construct the entire data object inline or import the application’s DTO.
11. Add Component interfaces only when substitution exists
An interface is useful when two or more Component implementations can fill the same role—for example, standard and accessible versions of a form.
When only one implementation exists, the Page Object should accept the concrete Component. An interface can be introduced when a second implementation creates a real need for substitution.
Why it matters: Interfaces make variation easier when variation exists. Before that point, they add ceremony without reducing coupling.
Do: Introduce an interface for two interchangeable payment Components.
Don’t: Require an interface for every class in the name of consistency.
12. Use Builders only for structural complexity
A Page Object Builder is justified when a page has at least two optional sub-Components, several structural configuration choices, or conditional construction logic.
Builders compose structure. Scenario data belongs in Page Object method arguments at execution time:
const checkoutPage = checkoutPageBuilder
.withPaymentComponent(stripePayment)
.withShippingComponent(domesticShipping)
.withGuestCheckout()
.build();
await checkoutPage.placeOrder(orderData);
For a fixed page with one Component, the fixture should construct the Page Object directly.
Why it matters: A Builder around a simple constructor is new with extra steps. Passing scenario data through it also mixes two concerns: what the page is made of and what the user is doing.
Do: Use a Builder for genuinely optional or conditional page structure.
Don’t: Add withFormData(data) to a Builder or use one for every Page Object.
13. Opt out when the Page Object metaphor breaks
Some interaction models resist clean user-intent methods. Graph editors, canvases, and complex drag-and-drop surfaces can require coordinate-based or continuous interactions that become misleading when forced behind conventional Page Object names.
An Assessment may opt out of the standard Object layer when this happens. It should remain in the domain, use its Fixtures, Network Mocks, and Data Variant Creators, and document why bespoke interaction helpers are necessary.
This is an exception for a mismatched interaction model, not a shortcut for difficult work. If most Assessments in a domain opt out, that domain is no longer meaningfully using CANDO-TEST.
Why it matters: A documented exception is more honest than an abstraction whose method names conceal what the interaction actually requires. Keeping the exception narrow prevents it from becoming a route around the architecture.
Do: Explain the opt-out at the top of the Assessment file.
Don’t: Use the rule because the page is unfamiliar or the standard structure takes longer to implement.
14. Enforce the folder convention in configuration
The test runner should discover tests only inside assessments/ directories. The structure should be enforced by configuration, not depend entirely on contributors remembering it.
Why it matters: If any test-shaped file can run from anywhere in the repository, code can bypass the domain fixture and the Assessment conventions while still appearing in the test results.
Do: Restrict the discovery pattern to domain assessments/ folders.
Don’t: Rely on a broad default pattern and a written convention alone.
The supporting ownership rules
The 14 rules define the main structure, but several ownership decisions make them work in practice.
Synchronisation belongs below the Assessment
Components wait for element-level state, such as an enabled button or a visible alert. Page Objects wait for page-level transitions, such as navigation or a modal closing. Assessments don’t add explicit delays; they invoke an assertion whose owning layer handles the required synchronisation.
Assertions belong to the observed surface
Components assert element state. Page Objects assert page-level outcomes. Network Mocks assert HTTP contracts when those contracts matter. Data Variant Creators never assert. Assessments declare the outcome by calling a named assertion on the layer that can observe it.
Tests remain independent
No Assessment should rely on state created by another. Data Variant Creators remain pure, Network Mocks have per-test state, and cross-test setup belongs in Fixtures.
Fixtures may use practical mechanisms such as API calls, cookies, or storage seeding to arrange state. Test repository autonomy governs what the suite asserts, not every mechanism it may use to prepare a scenario.
Rules should protect against a known failure
A framework can accumulate rules in the same way an application accumulates abstractions: one reasonable addition at a time, until nobody remembers why half of them exist.
Each CANDO-TEST rule should be traceable to a failure it prevents. The rule against locators in Page Objects protects element-level encapsulation. The rule against inline data protects scenario clarity. The limits on interfaces and Builders protect against abstraction inflation.
That gives reviewers a useful test: if we can’t explain the failure a rule prevents in this situation, it may not be worth enforcing there.
The goal isn’t architectural purity. It is a test suite in which intent remains readable, changes remain local, and exceptions remain visible. The rules give that structure enough strength to survive the small shortcuts that otherwise accumulate unnoticed.





