Design Challenge 008: Testing the Untestable Component
A deeper look at verifying headless components without relying on real dependencies.
Welcome back to the Design Challenges series! In a previous issue, I walked through using render props to centralize logic and maintain flexible UI. This is a textbook use case of the Headless Component Pattern. In that example, we introduced an ApprovalService
component responsible for managing the request-approval flow, while leaving UI details up to the consumer.
Here’s a snippet of how ApprovalService
might look:
const ApprovalService = ({ id, children }) => {
const [isDone, setDone] = useState(false);
const handleApprove = () => {
fetch(`/rest/approval/${id}/approve`, { method: "POST" })
.then((r) => r.json())
.then((data) => setDone(data.isDone));
};
const handleDecline = () => {
fetch(`/rest/approval/${id}/decline`, { method: "POST" })
.then((r) => r.json())
.then((data) => setDone(data.isDone));
};
return <>{children({ isDone, handleApprove, handleDecline })}</>;
};
The pure “headless” piece of this design is just the state and logic. Because there’s no UI in ApprovalService
, consumers can decide how best to display the approval status. For example:
const ApprovalPanel = ({ id }) => {
return (
<ApprovalService id={id}>
{({ isDone, handleApprove, handleDecline }) => {
if (isDone) {
return <div>The request has been resolved</div>;
}
return (
<div className={classes.container}>
<h2>This request requires your approval</h2>
<div className={classes.buttonsContainer}>
<Button onClick={handleApprove} appearance="Primary">
Approve
</Button>
<Button onClick={handleDecline}>Decline</Button>
</div>
</div>
);
}}
</ApprovalService>
);
};
The Challenge
In a typical integration test, you might check if clicking the “Approve” button triggers an actual request. However, what if you don’t want to— or can’t— rely on a network layer? Perhaps you’re writing unit tests, or your CI environment isolates network calls. How do you effectively verify that:
ApprovalPanel
updates the UI when the approval is complete, without hooking into the real API?ApprovalService
can be tested independently, focusing purely on its “headless” behaviour?
Your challenge is to write tests for these scenarios in a way that ensures:
The logic inside
ApprovalService
works as expected (e.g. state transitions from “pending” to “done”).The UI of
ApprovalPanel
responds to those transitions without actually sending real requests.
This will clarify how you can isolate tests for headless components as well as the UI they power. It’s a two-sided challenge: one set of tests for the “invisible” logic, and another for the “visible” outcome.
Over to You
How would you mock or stub out the network layer in your tests?
Would you test
ApprovalService
in isolation, or only throughApprovalPanel
?How would you confirm the UI properly reflects changes in
isDone
?
If you have missed the previous issue on the headless component - ApprovalPanel, you can find them here (the problem and solution). As always, I’ll share my solution in an issue next week.
Feel free to reply in the comments or send me a message. I’ll be back soon with my take on testing “untestable” components—particularly those deeply buried in the application logic. Happy testing!
Stay tuned for the solution and discussion in the next issue of Design Challenges.