Introduction
The path to better testing starts with something surprisingly simple: how you name your tests. Good test names:
- Make your test suite more maintainable
- Guide you toward writing tests that focus on user behavior
- Improve clarity and readability for your team
In this blog post, we’ll explore 10 essential rules for writing better tests that will transform your approach to testing. These principles are:
- Framework-agnostic
- Applicable across the entire testing pyramid
- Useful for various testing tools:
- Unit tests (Jest, Vitest)
- Integration tests
- End-to-end tests (Cypress, Playwright)
By following these rules, you’ll create a more robust and understandable test suite, regardless of your chosen testing framework or methodology.
Rule 1: Always Use “should” + Verb
Every test name should start with “should” followed by an action verb.
// ❌ Bad
it("displays the error message", () => {});
it("modal visibility", () => {});
it("form validation working", () => {});
// ✅ Good
it("should display error message when validation fails", () => {});
it("should show modal when trigger button is clicked", () => {});
it("should validate form when user submits", () => {});
Generic Pattern: should [verb] [expected outcome]
Rule 2: Include the Trigger Event
Specify what causes the behavior you’re testing.
// ❌ Bad
it("should update counter", () => {});
it("should validate email", () => {});
it("should show dropdown", () => {});
// ✅ Good
it("should increment counter when plus button is clicked", () => {});
it("should show error when email format is invalid", () => {});
it("should open dropdown when toggle is clicked", () => {});
Generic Pattern: should [verb] [expected outcome] when [trigger event]
Rule 3: Group Related Tests with Descriptive Contexts
Use describe blocks to create clear test hierarchies.
// ❌ Bad
describe("AuthForm", () => {
it("should test empty state", () => {});
it("should test invalid state", () => {});
it("should test success state", () => {});
});
// ✅ Good
describe("AuthForm", () => {
describe("when form is empty", () => {
it("should disable submit button", () => {});
it("should not show any validation errors", () => {});
});
describe("when submitting invalid data", () => {
it("should show validation errors", () => {});
it("should keep submit button disabled", () => {});
});
});
Generic Pattern:
describe("[Component/Feature]", () => {
describe("when [specific condition]", () => {
it("should [expected behavior]", () => {});
});
});
Rule 4: Name State Changes Explicitly
Clearly describe the before and after states in your test names.
// ❌ Bad
it("should change status", () => {});
it("should update todo", () => {});
it("should modify permissions", () => {});
// ✅ Good
it("should change status from pending to approved", () => {});
it("should mark todo as completed when checkbox clicked", () => {});
it("should upgrade user from basic to premium", () => {});
Generic Pattern: should change [attribute] from [initial state] to [final state]
Rule 5: Describe Async Behavior Clearly
Include loading and result states for asynchronous operations.
// ❌ Bad
it("should load data", () => {});
it("should handle API call", () => {});
it("should fetch user", () => {});
// ✅ Good
it("should show skeleton while loading data", () => {});
it("should display error message when API call fails", () => {});
it("should render profile after user data loads", () => {});
Generic Pattern: should [verb] [expected outcome] [during/after] [async operation]
Rule 6: Name Error Cases Specifically
Be explicit about the type of error and what causes it.
// ❌ Bad
it("should show error", () => {});
it("should handle invalid input", () => {});
it("should validate form", () => {});
// ✅ Good
it('should show "Invalid Card" when card number is wrong', () => {});
it('should display "Required" when password is empty', () => {});
it("should show network error when API is unreachable", () => {});
Generic Pattern: should show [specific error message] when [error condition]
Rule 7: Use Business Language, Not Technical Terms
Write tests using domain language rather than implementation details.
// ❌ Bad
it("should update state", () => {});
it("should dispatch action", () => {});
it("should modify DOM", () => {});
// ✅ Good
it("should save customer order", () => {});
it("should update cart total", () => {});
it("should mark order as delivered", () => {});
Generic Pattern: should [business action] [business entity]
Rule 8: Include Important Preconditions
Specify conditions that affect the behavior being tested.
// ❌ Bad
it("should enable button", () => {});
it("should show message", () => {});
it("should apply discount", () => {});
// ✅ Good
it("should enable checkout when cart has items", () => {});
it("should show free shipping when total exceeds $100", () => {});
it("should apply discount when user is premium member", () => {});
Generic Pattern: should [expected behavior] when [precondition]
Rule 9: Name UI Feedback Tests from User Perspective
Describe visual changes as users would perceive them.
// ❌ Bad
it("should set error class", () => {});
it("should toggle visibility", () => {});
it("should update styles", () => {});
// ✅ Good
it("should highlight search box in red when empty", () => {});
it("should show green checkmark when password is strong", () => {});
it("should disable submit button while processing", () => {});
Generic Pattern: should [visual change] when [user action/condition]
Rule 10: Structure Complex Workflows Step by Step
Break down complex processes into clear steps.
// ❌ Bad
describe("Checkout", () => {
it("should process checkout", () => {});
it("should handle shipping", () => {});
it("should complete order", () => {});
});
// ✅ Good
describe("Checkout Process", () => {
it("should first validate items are in stock", () => {});
it("should then collect shipping address", () => {});
it("should finally process payment", () => {});
describe("after successful payment", () => {
it("should display order confirmation", () => {});
it("should send confirmation email", () => {});
});
});
Generic Pattern:
describe("[Complex Process]", () => {
it("should first [initial step]", () => {});
it("should then [next step]", () => {});
it("should finally [final step]", () => {});
describe("after [key milestone]", () => {
it("should [follow-up action]", () => {});
});
});
Complete Example
Here’s a comprehensive example showing how to combine all these rules:
// ❌ Bad
describe("ShoppingCart", () => {
it("test adding item", () => {});
it("check total", () => {});
it("handle checkout", () => {});
});
// ✅ Good
describe("ShoppingCart", () => {
describe("when adding items", () => {
it("should add item to cart when add button is clicked", () => {});
it("should update total price immediately", () => {});
it("should show item count badge", () => {});
});
describe("when cart is empty", () => {
it("should display empty cart message", () => {});
it("should disable checkout button", () => {});
});
describe("during checkout process", () => {
it("should validate stock before proceeding", () => {});
it("should show loading indicator while processing payment", () => {});
it("should display success message after completion", () => {});
});
});
Test Name Checklist
Before committing your test, verify that its name:
- Starts with “should”
- Uses a clear action verb
- Specifies the trigger condition
- Uses business language
- Describes visible behavior
- Is specific enough for debugging
- Groups logically with related tests
Conclusion
Thoughtful test naming is a fundamental building block in the broader landscape of writing better tests. To maintain consistency across your team:
- Document your naming conventions in detail
- Share these guidelines with all team members
- Integrate the guidelines into your development workflow
For teams using AI tools like GitHub Copilot:
- Incorporate these guidelines into your project documentation
- Link the markdown file containing these rules to Copilot
- This integration allows Copilot to suggest test names aligned with your conventions
For more information on linking documentation to Copilot, see: VS Code Experiments Boost AI Copilot Functionality
By following these steps, you can ensure consistent, high-quality test naming across your entire project.