CANDO-TEST: A clearer architecture for UI test automation
A tool-agnostic approach to separating test intent from UI mechanics as an automation 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.
CANDO-TEST: A clearer architecture for UI test automation
My wife’s move into test automation made me look again at a problem I had started to take for granted.
She was entering the field without a background in QA or IT. Alongside learning the tools and syntax, she also had to work out where everything belonged. Which code should describe a user journey? Where should selectors live? Who creates the test data? When does a Page Object help, and when does it simply add another layer to navigate?
Those questions aren’t limited to people who are new to automation. A test suite can be easy to begin and surprisingly difficult to structure. The first few tests may be clear enough, but as the suite grows, responsibilities blur. Selectors appear in test cases, Page Objects become collections of unrelated helpers, setup is duplicated, and small interface changes ripple through dozens of files.
CANDO-TEST is my attempt to give that complexity a shape.
It is a structured approach to UI test automation, built around explicit responsibilities and one-way dependencies. It isn’t a library or a wrapper around Playwright, Selenium, Cypress, or another tool. The underlying APIs will differ, but the architectural problem remains much the same: keeping the intent of a test separate from the mechanics required to execute it.
The five building blocks
CANDO-TEST takes its name from five parts of a test system:
| Letter | Layer | Responsibility |
|---|---|---|
| C | Components | Reusable UI regions, including their locators, element-level interactions, and assertions |
| A | Assessments | The behaviour being verified and the conditions under which it should occur |
| N | Network Emulation | Controlled HTTP responses, errors, delays, and other network behaviour |
| D | Data Variant Creators | Named, typed test-data scenarios |
| O | Objects | Page Objects that express user actions, plus Page Object Builders when composition is genuinely complex |
There is also a sixth supporting layer: Fixtures. Fixtures create and connect the other parts, provide each test with the dependencies it needs, and own cross-cutting setup such as authentication or base URLs. They aren’t part of the acronym, but they become essential as a suite grows.
The names are useful, but the boundaries between them matter more. CANDO-TEST works when each layer owns one kind of knowledge and dependencies move in a consistent direction.
The central rule: dependencies move downwards
The main chain is deliberately small:
Assessment
↓
Page Object
↓
Component
An Assessment knows about a Page Object because it needs to express a scenario. A Page Object knows about Components because it coordinates parts of a page. A Component knows how to interact with the elements it owns.
The knowledge never travels back up. A Component doesn’t know which Page Objects use it. A Page Object doesn’t know which Assessments exercise it. This keeps lower layers reusable and prevents the suite from becoming a graph of mutual dependencies.
Network Emulation and Data Variant Creators sit alongside this chain. Assessments use them to declare conditions, but Page Objects and Components don’t need to know they exist. Fixtures are the composition root: they are allowed to reach across the architecture to assemble the dependencies, while the dependencies themselves remain unaware of the fixture.
This distinction may sound formal, but it has a practical effect. When a selector changes, the Component changes. When a user workflow changes, the Page Object changes. When the conditions of a scenario change, the Assessment changes. Each kind of change has a natural home.
Assessments should read like specifications
I use the term Assessment for a test case because it emphasises purpose. An Assessment verifies observable behaviour under stated conditions; it shouldn’t expose the mechanics of locating elements or wiring infrastructure.
An Assessment has two phases.
The first is Configuration: choose a data variant and determine how the network should behave. This answers, “Given these conditions…”
The second is Execution: perform the user action and verify what becomes observable. This answers, “…when the user does this, what happens?”
For example:
test("shows a success confirmation", async ({
todoPage,
todoApiNetworkMock,
todoDataVariantCreator,
}) => {
// Configuration
const data = todoDataVariantCreator.createValidData();
await todoApiNetworkMock.withSuccessFetchResponse().mock();
// Execution
await todoPage.open();
await todoPage.createTodo(data);
await todoPage.assertCreatedSuccessfully();
});
The test says what matters without revealing how an input is found, how the submit action works, or how the success state is detected. Those details still exist and remain traceable, but they live in the layers that own them.
Configuration isn’t incidental setup to hide in a hook. It is part of the scenario. Keeping it visible makes the conditions of the Assessment clear.
Components own elements; Page Objects own user actions
The boundary between Components and Page Objects is easy to blur.
A Component represents a meaningful region of the interface. It owns locators, element-level interactions, element-level assertions, and the waiting required to make those interactions reliable. A form Component might fill fields, submit the form, and detect validation messages.
Components should be created when a UI region contains behaviour worth encapsulating, has a natural domain boundary, or will be reused. Wrapping every heading and button in its own class only replaces DOM complexity with object complexity.
Shared base Components can hold recurring interaction protocols. If several domains contain similar forms, a base form Component might define submission and error handling while each domain Component supplies its particular fields and locators. I prefer to extract these bases after a pattern appears in more than one domain rather than predict reuse in advance.
A Page Object works one level above that. It speaks in the language of a user and coordinates Components across a page:
await todoPage.createTodo(data);
await todoPage.navigateToSettings();
await todoPage.assertCreatedSuccessfully();
Methods such as clickSubmitButton() or fillTextField() reveal element mechanics and belong in a Component. Page Objects should describe intent, navigation, page-level transitions, and outcomes involving more than one part of the interface.
Page Objects also shouldn’t form a nested graph. A dashboard Page Object may trigger navigation to settings, but it shouldn’t construct or return a Settings Page Object. Fixtures can build each page independently. Reusable regions belong in Components, not inside other Page Objects.
Test data should describe scenarios
Inline data objects often make an Assessment look self-contained, but they obscure why those values were chosen and encourage duplication.
Data Variant Creators give scenarios names:
const data = todoDataVariantCreator.createValidData();
const overdue = todoDataVariantCreator.createWithPastDueDate();
const urgent = todoDataVariantCreator.createValidData({
priority: "high",
});
Scoped overrides are useful when an Assessment needs to pin one dimension of a recognised scenario. They shouldn’t replace the scenario with an arbitrary object assembled in the test.
Variant Creators are pure data factories. They don’t make network calls or assert outcomes, and they own their types inside the test repository.
That last constraint is deliberate. CANDO-TEST treats the test repository as an autonomous project that observes the application through its running interface. It doesn’t import runtime schemas, components, or types from the application under test. Some duplication at this boundary is valuable: if the application and the independently defined test contract diverge, the test should reveal it. Importing the application’s own schema risks testing whether the application agrees with itself.
Network behaviour is part of the condition
Reliable UI tests need controlled network behaviour. The Network Emulation layer owns HTTP interception and simulated responses: success, validation errors, empty results, expired sessions, timeouts, and ordered responses for retry flows.
Its methods can use domain language when that makes an Assessment easier to read:
await todoApiNetworkMock.withPermissionDenied().mock();
But the layer remains focused on transport. It answers, “What does the network do?” It doesn’t drive Page Objects or infer whether a user journey succeeded.
Assertions follow the same ownership rule used elsewhere. A Network Mock can verify an HTTP-level contract such as a request method or payload. A Component can verify an element-level result. A Page Object can verify a page-level outcome. The Assessment states which outcome matters and delegates the mechanics to the layer that owns the observable surface.
Fixtures connect the architecture
Manual construction inside every test or setup hook spreads infrastructure knowledge across the suite. CANDO-TEST instead uses Fixtures as an Inversion of Control composition root.
A shared session fixture owns cross-cutting concerns such as authentication, the base URL, headers, or cache state. Each domain fixture extends it and creates that domain’s Page Objects, Network Mocks, and Data Variant Creators. Assessments import the test entry point from their domain fixture rather than directly from the automation framework.
This gives dependencies a clear lifecycle and makes the wiring inspectable in one place. It also keeps Assessments focused on the scenario rather than object construction.
The folder structure follows the same principle. Files are grouped by the user-facing domain they belong to, not by their technical type:
src/
shared/
components/
fixtures/
todo/
assessments/
components/
dataVariants/
fixtures/
networkMocks/
objects/
When a feature changes, its test code changes together. Understanding or removing a domain doesn’t require searching through six unrelated top-level directories.
Architecture should remove complexity, not manufacture it
Structure can become its own form of clutter. CANDO-TEST includes a few explicit limits to resist that.
Interfaces are useful when two or more Component implementations can occupy the same role. With one implementation, accepting the concrete class is clearer.
Page Object Builders are useful when a page has multiple optional Components, several structural choices, or conditional construction. For a fixed page with one Component, a Builder is little more than new with extra steps. Builders configure structure, not scenario data; data remains an argument to a user action.
Some user interfaces, such as canvas editors or complex drag-and-drop surfaces, may not fit the Page Object metaphor honestly. In those cases, a documented opt-out with purpose-built helpers can be better than inventing misleading user-language methods. It should remain an exception: if most tests need to bypass the Object layer, the framework is no longer providing the intended structure.
These constraints reflect a broader principle. An abstraction should earn its existence by reducing duplication, isolating change, or making intent clearer. Consistency alone isn’t enough.
What CANDO-TEST is trying to protect
UI automation tends to erode through small, reasonable-looking shortcuts: one selector added to a test, one Page Object that reaches into another, one inline data object, one explicit wait, or one shared base created before there is anything to share.
Any one of these may seem harmless. Repeated across a suite, they make tests harder to read and changes harder to contain.
CANDO-TEST is a way to make those architectural decisions visible. It provides names for the parts, rules for how they depend on one another, and escape hatches for the cases that don’t fit. It deliberately leaves tool choice, timeout policy, coverage targets, and enforcement mechanisms to the teams using it.
My wife’s willingness to step into an unfamiliar field prompted the first version of this idea. Developing it further has changed how I think about test architecture: confidence doesn’t come from hiding complexity. It comes from giving each part of the system a clear responsibility, then keeping the path between those parts easy to follow.
That is the promise of CANDO-TEST. Not less code at any cost, and not abstraction for its own sake, but a test suite whose structure helps people understand what it is verifying and where to make the next change.





