243: Testing Under Time Pressure: Behavior, Async, Accessibility, and Edge Cases
Learning outcomes
By the end of this lesson, you can:
- explain and apply pure logic tests in a realistic implementation;
- explain and apply component behavior in a realistic implementation;
- explain and apply async tests in a realistic implementation;
- explain and apply accessibility checks in a realistic implementation;
- explain and apply edge-case prioritization in a realistic implementation.
Prerequisites and retrieval
This lesson assumes the earlier 01–06 foundation and the preceding lessons in this module. Before you read, retrieve one concrete example from a previous project in which one of these concerns appeared. Perhaps a reducer mishandled an empty collection, a loading indicator never settled after a failed request, or a keyboard user could not reach an action. The point is not to memorize a vocabulary list. In a timed machine-coding exercise, you need to make a defensible decision while keeping the implementation readable, accessible, testable, and straightforward to extend under interview pressure.
Terminology
- Pure logic tests: Tests for reducers, parsers, sorting/filtering, and domain rules that can run quickly with table-driven cases. They are most useful when the behavior can be checked without a browser, network, clock, or other external dependency.
- Component behavior: Tests that query elements by accessible role or label and assert what a user can see and do, rather than inspecting internal state or private component methods.
- Async tests: Tests that wait for observable state transitions, such as loading to success or loading to error, instead of depending on fixed sleeps.
- Accessibility checks: Tests for labels, accessible names, focus movement, keyboard paths, and, where useful, automated axe checks. Automation is a supplement to interaction tests, not a replacement for them.
- Edge-case prioritization: Choosing edge cases from the requirements. Empty results, duplicate input, a failed request, a slow request, very long text, and repeated user action often expose more real defects than an arbitrary coverage target.
- E2E restraint: Recognizing that a timed challenge rarely needs a large end-to-end suite. The highest-value checks are the ones that exercise a meaningful contract without consuming the whole implementation window.
Mental model
Treat Testing Under Time Pressure: Behavior, Async, Accessibility, and Edge Cases as a design problem. Identify the observable inputs and outputs, the invariants that must stay true, and the ways the system can fail. A small set of deliberately chosen tests often demonstrates more engineering maturity than broad, low-value coverage. In a timed round, test the contracts most likely to break as you implement and refactor.
A strong implementation makes its assumptions visible, reduces uncertainty at boundaries, and leaves evidence that the design is safe. That evidence might be tests, types, constraints, metrics, or diagrams. The test strategy is therefore part of the design, not a task reserved for the final five minutes.
A useful interview and production sequence is:
requirement -> constraints -> model -> implementation -> failure analysis -> verification
Do not jump from a requirement directly to a library call. First state what must remain true. Then select the mechanism that enforces it and the observation that proves it. For example, “a submit action creates at most one item for one user intent” is a clearer contract than “use a mutation hook.”
Deep dive
1. Pure logic tests
Reducers, parsers, sorting/filtering functions, and domain rules are usually the fastest places to obtain useful confidence. A table-driven test can make the input, expected output, and important variation visible in one place. Because the test does not need a browser or network, it remains fast during repeated refactors.
The useful distinction is between testing a stable rule and testing an implementation detail. If a reducer must preserve unrelated state while handling an empty result, assert that rule directly. If a parser rejects malformed input, include the malformed shape and the expected failure. Keep the test close to the domain contract rather than coupling it to a particular helper call.
Decision rule: Use pure logic tests deliberately when they make a contract or invariant easier to prove. If extracting logic only reduces typing while hiding an assumption, prefer the more explicit design.
2. Component behavior
Component tests should describe the interaction a user can perform and the result the user can observe. Query by accessible role, label, or name, then assert visible output, enabled or disabled actions, and other meaningful behavior. This avoids coupling the test to private state, implementation-specific class names, or component methods that users never call.
This is where people usually get confused: “behavior” does not mean ignoring implementation completely. It means choosing the public behavior as the contract. Internal state matters only through its effect on the rendered interface or an interaction. A button that appears after a successful request should be found and used as a button, not located by a state variable that happens to control it.
Decision rule: Use component behavior tests deliberately when they make the user-facing contract or invariant easier to prove. If a test can pass while the user cannot find, focus, or operate the control, it is asserting the wrong boundary.
3. Async tests
Asynchronous UI has state transitions, not merely a delayed final value. A realistic test should observe loading, then wait for success or failure using the test runner's asynchronous queries and utilities. Fixed sleeps are brittle: they may be too short on a busy run and unnecessarily slow when the request completes immediately.
Mock network boundaries at a realistic layer so the component still executes its loading, retry, error, and recovery behavior. The test should prove what the caller observes when the dependency resolves or rejects. If cancellation, stale results, or duplicate requests are part of the requirements, model those cases explicitly rather than assuming that the happy-path promise is enough.
Decision rule: Use async tests deliberately when they make a state transition or failure contract easier to prove. If the test waits for an arbitrary duration instead of a condition, it is probably hiding a race or missing an observable signal.
4. Accessibility checks
Accessibility testing starts with the structure and interactions a user needs: meaningful labels and accessible names, correct roles, sensible focus movement, and complete keyboard paths. Exercise the interface with keyboard input where the requirement includes keyboard use. Automated axe checks can identify many structural problems quickly, but they cannot prove that the interaction makes sense or that focus moves to the right place after an update.
Keep the semantic contract visible in the test. A control that is visually styled as a button but has no button semantics may look correct in a screenshot while remaining unusable to keyboard or assistive-technology users. The check should therefore verify the user-facing name and role, not just that some element exists.
Decision rule: Use accessibility checks deliberately when they make the interaction contract or invariant easier to prove. Treat automated scanning as complementary coverage; it does not replace testing labels, names, focus movement, or keyboard behavior.
5. Edge-case prioritization
Do not choose edge cases only because they are easy to enumerate. Start with the requirements and the boundaries where the design can violate them: empty results, duplicate input, failed requests, slow requests, very long text, and repeated user action. These cases expose missing states, race conditions, accidental duplicate writes, layout assumptions, and unclear recovery behavior.
Under interview time pressure, prioritize by risk and observability. One test that proves duplicate-submit prevention may be worth more than several shallow tests of unrelated markup. For each selected case, state the expected user-visible result and the layer responsible for enforcing the rule.
Decision rule: Use edge-case prioritization deliberately when it makes the contract or invariant easier to prove. If it only produces a longer checklist without connecting cases to requirements, reduce the list and test the boundaries with the highest failure cost.
6. E2E restraint
A timed challenge rarely needs a large E2E suite. If setup already exists, one critical journey can be valuable because it verifies that the main pieces work together. Otherwise, component and integration tests usually provide better signal per minute and are easier to diagnose when they fail.
The trade-off is diagnostic depth. An E2E failure may involve routing, rendering, the network, or test data, while a focused component test can identify the broken contract directly. Choose the smallest layer that proves the behavior, and reserve E2E coverage for a journey whose cross-boundary value justifies its setup and maintenance cost.
Decision rule: Use E2E restraint deliberately when it makes the contract or invariant easier to prove. If adding an end-to-end test consumes the time needed to test loading, error, keyboard, and duplicate-action behavior, the broader test is not the better choice for this exercise.
Worked example
Consider a timed machine-coding exercise that must remain readable, accessible, testable, and easy to extend under interview pressure. Start by writing the requirement in one sentence. Then list the input and output contracts and identify which concept above owns each likely failure mode.
The important design move is separation. Parsing and validation belong at the boundary; domain rules belong in the domain or service layer; persistence rules belong in the database or repository; and presentation rules belong in the client. Mixing these concerns can make a happy-path demo look shorter, but it makes edge cases much harder to reason about and test. A component should be able to represent loading, empty, error, stale, and success states without also becoming the authority for persistence or authorization.
For a client that reads and creates items, the dependency boundary might look like this:
const listQuery = useQuery({
queryKey: ['items', filters],
queryFn: ({ signal }) => api.items.list({ filters, signal }),
});
const createItem = useMutation({
mutationFn: api.items.create,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
});
The code does not by itself prove that the UI is correct. Test the observable states around it. Walk through at least four cases: the normal path; an empty or missing value; a duplicate, retry, or concurrent path where relevant; and a dependency failure. For each case, state which layer detects the problem and what the caller observes. For example, the client may show a validation message for a missing value, while the service or database must enforce uniqueness when two requests race. This is the level of explanation expected in a senior code review or technical interview.
Production perspective
Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high-cardinality workloads. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.
When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume the client can be modified and network input is untrusted. Client-side tests can prove a UI response; they cannot replace server-side authorization, validation, or persistence guarantees.
Guided lab
Add five high-signal tests to an existing challenge: a reducer edge case, loading-to-success, server error, keyboard interaction, and duplicate-submit prevention. For each test, name the contract it protects and explain why you did not test implementation details. A test should fail when the user-visible behavior is wrong, not merely when a private function is renamed.
Complete the lab with this discipline:
- Write the requirement and two non-requirements.
- List input, output, and error contracts before implementation.
- Implement the smallest correct vertical slice.
- Add at least one invalid-input test and one edge-case test.
- Instrument or inspect the behavior instead of guessing.
- Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
- Explain one alternative design and why you did not choose it.
- Record a short “what would break at 10× scale?” note.
The lab is intentionally broader than a single happy-path assertion. It makes you practice the sequence from requirement to verification while keeping the test suite small enough for a machine-coding setting.
Edge cases and failure modes
- Pure logic tests: Test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Confirm both the returned value and any invariant that must remain unchanged.
- Component behavior: Test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Assert what the user can find, see, focus, and operate.
- Async tests: Test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Observe transitions and failures rather than relying on elapsed time.
- Accessibility checks: Test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Include names, focus movement, and keyboard paths when the interaction requires them.
- Edge-case prioritization: Test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Select cases from actual requirements and risk instead of chasing arbitrary coverage.
Common mistakes and debugging
- Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
- Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary”
anyvalues. - Testing only the happy path and therefore discovering contracts only after integration.
- Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
- Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.
For debugging, reproduce the smallest failing case first. Inspect the actual value or execution plan, trace the boundary where the invariant first becomes false, and fix the owning layer instead of adding a downstream patch. In a test failure, determine whether the test observed the wrong behavior, waited at the wrong boundary, or encoded an implementation detail. In an async failure, inspect the request sequence and state transitions; in an accessibility failure, inspect the rendered role, accessible name, and focus target.
Interview questions
- What problem do Pure logic tests solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does Component behavior solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do Async tests solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do Accessibility checks solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does Edge-case prioritization solve, and what trade-off or failure mode would make you choose a different approach?
Checkpoint
Without notes, explain Testing Under Time Pressure: Behavior, Async, Accessibility, and Edge Cases to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code. Be prepared to explain which observation proves each behavior and which layer owns each failure.
Mastery checklist
- I can define the core terms precisely.
- I can choose a design from requirements instead of from habit.
- I can implement and test the normal path and edge cases.
- I can explain the runtime, storage, or complexity cost.
- I can identify which layer owns validation, errors, and recovery.
- I can compare at least two reasonable alternatives.
- I can explain how the design changes at larger scale or stricter reliability.
