As applications grow, tests often become harder to understand than the code they are supposed to verify.
A common problem is that tests focus heavily on implementation details and not enough on behavior. While the assertions may be correct, someone reading the test can struggle to understand what scenario is actually being validated.
The Given–When–Then pattern helps solve this problem by structuring tests around behavior rather than implementation.
Instead of thinking about a test as a sequence of setup code and assertions, you think about it as a story:
- Given a certain state exists.
- When an action occurs.
- Then a specific outcome should happen.
This approach is particularly useful in frontend applications because it closely matches how requirements are usually described by product managers, designers, and users.
Thinking in Scenarios
Consider a requirement such as "a user can only create a course after entering a title". A typical test might look like this:
it("enables the create button after entering a title", async () => {
render(<CreateCourseForm />);
await user.type(
screen.getByLabelText("Title"),
"React Fundamentals"
);
expect(
screen.getByRole("button", {
name: /create course/i,
})
).toBeEnabled();
});
The test works, but the scenario is not immediately obvious. Using Given–When–Then, the structure becomes much more explicit:
describe("Given there are organizations available", () => {
describe("When the user types a title", () => {
it("Then the create course button becomes enabled", async () => {
render(<CreateCourseForm />);
await user.type(
screen.getByLabelText("Title"),
"React Fundamentals"
);
expect(
screen.getByRole("button", {
name: /create course/i,
})
).toBeEnabled();
});
});
});
Now the test reads almost like a sentence. Someone unfamiliar with the implementation can understand the scenario without reading the component itself.
Testing Form Validation
This style is particularly useful for forms because forms naturally contain multiple states and user interactions.
Imagine a registration form where the submit button should remain disabled until all required fields are completed.
describe("Given the registration form is displayed", () => {
describe("When the user has not completed all required fields", () => {
it("Then the submit button remains disabled", () => {
render(<RegistrationForm />);
expect(
screen.getByRole("button", {
name: /register/i,
})
).toBeDisabled();
});
});
});
And the successful scenario:
describe("Given the registration form is displayed", () => {
describe("When the user completes all required fields", () => {
it("Then the submit button becomes enabled", async () => {
render(<RegistrationForm />);
await user.type(
screen.getByLabelText("Email"),
"john@example.com"
);
await user.type(
screen.getByLabelText("Password"),
"secret123"
);
expect(
screen.getByRole("button", {
name: /register/i,
})
).toBeEnabled();
});
});
});
Notice how the test structure emphasizes behavior rather than implementation.
Testing Business Rules
The pattern is equally valuable when testing domain logic. Imagine an application where premium users receive a discount during checkout.
describe("Given the customer has a premium membership", () => {
describe("When the final price is calculated", () => {
it("Then a 20 percent discount is applied", () => {
const premiumUser = {
membership: "premium",
};
const finalPrice = calculatePrice(
100,
premiumUser
);
expect(finalPrice).toBe(80);
});
});
});
The test now resembles the business requirement itself. This makes it easier to discuss, review, and maintain.
Testing Permissions
Permissions are another area where Given–When–Then shines because access rules are often described in natural language. Consider an admin-only section.
describe("Given the user is not an administrator", () => {
describe("When they try to access the admin page", () => {
it("Then access is denied", () => {
const result = canAccessAdminPage({
role: "user",
});
expect(result).toBe(false);
});
});
});
And the corresponding positive case:
describe("Given the user is an administrator", () => {
describe("When they try to access the admin page", () => {
it("Then access is granted", () => {
const result = canAccessAdminPage({
role: "admin",
});
expect(result).toBe(true);
});
});
});
The tests effectively document the permission model of the application.
When Does This Style Make Sense?
This style works best when tests represent business scenarios, user interactions, validation rules, permissions, workflows, or state transitions.
The more important the behavior is, the more valuable the Given–When–Then structure becomes.
For very small utility functions, the extra nesting may not provide much value. But for features where understanding the scenario matters, the structure can dramatically improve readability.
A useful rule is that someone should be able to understand the intent of the test simply by reading the describe and it blocks.
Conclusions
The biggest benefit of Given–When–Then is not the structure itself, but the way it encourages developers to think. Rather than testing functions and components directly, you start testing scenarios and behaviors.
When used consistently, the test suite becomes more than a collection of assertions. It becomes living documentation that explains how the system is expected to behave.
Top comments (0)