1. 09Feature modules are units of change
  2. 10The parts of a feature
  3. 11Making a feature ready to build
  4. 12Building a feature from idea to implementation
  5. 13Testing a feature without the host
  6. 14The application is the composer

Building product features

13

Testing a feature without the host

Prove interface behavior without booting the application shell, authentication, or live services.

By Eyal Ellenbogen August 27, 2026 3 min read

Test feedback should point straight to the problem, not hand us a crime scene to investigate.

When a test isolates user interface behavior, every assertion answers a specific question: Does the empty state render when a list has zero items? Does the form stay populated when a save fails? Are buttons disabled while a request is in flight?

Our goal is to prove the interface across those mapped states without connecting to a live server. Feature tests verify how the screen responds to data, while a smaller contract test verifies that the real backend payload matches what the feature expects.

Turn the state map into the test suite

Refinement has already identified the cases worth testing: loading, an empty list, validation errors, disabled controls during a Save operation, unauthorized access, and recovery after a server failure. Each becomes a deliberate demo scenario rather than a condition our test suite has to find by manipulating a staging account.

The same in-memory port that drives the demo can give a test an exact failure mode:

const failingPort: MemberInvitePort = {
	load: async () => ({ pendingInvites: [] }),
	send: async () => {
		throw new Error('Quota exceeded')
	},
}

This makes the test about the user-visible result. After a failed invitation, does the recovery message appear and does the form retain the email the user entered? It runs without a network, a seeded account, or a browser pointed at a shared environment. If a scenario cannot be reached without those dependencies, we should improve the demo before expanding the test.

Verify contract mapping at the perimeter

Keeping network traffic out of feature tests does not remove the need to test backend integration. It puts that test where the integration lives: in the production adapter.

The adapter maps incoming payloads into the models exposed by the feature port, then turns UI commands into valid requests. A focused mapping test can use a recorded response or a typed fixture without rendering a screen:

const member = mapMemberDto({
	id: 'usr_481',
	user_email: 'sarah@example.com',
	role_flags: ['CAN_VIEW', 'CAN_EXPORT'],
})

expect(member).toEqual({
	id: 'usr_481',
	email: 'sarah@example.com',
	canManage: false,
	canExport: true,
})

When a backend field or enum changes, this check points directly to the mapper. The feature tests may still pass because their port implementation is intentionally independent of the transport format, which is exactly why the two checks should remain separate.

What end-to-end tests actually prove

Keeping feature interactions in local tests does not eliminate end-to-end testing. It makes end-to-end testing valuable again.

We still need end-to-end tests. A real browser navigating through real authentication, hitting real gateways, and loading a real database is the only check that proves the entire system hangs together. That check is essential before shipping to customers.

The trap is using end-to-end tests to verify every edge case. When a suite tries to validate form field validations, modal error banners, and empty list states through full staging runs, tests become slow, flaky, and expensive to maintain.

We can reserve end-to-end tests for critical integration paths:

  • Can an authenticated user navigate to the feature route?
  • Does the production adapter successfully talk to the live gateway?
  • Does completing a core workflow persist data across the stack?

That division gives every test a clear purpose. Feature tests exhaustively verify user interactions and edge cases quickly against the demo. Contract tests verify that the adapter translates payloads correctly. A few focused end-to-end tests then prove that the application composer, live services, and browser work as a cohesive whole.