Testing Patterns and Utilities
- Never write production code without a failing test
Category: content-creation Source: ChrisWiles/claude-code-showcaseWhat Is This
The "Testing Patterns and Utilities" skill provides a practical, opinionated approach to writing effective automated tests for JavaScript and TypeScript projects-particularly those using Jest and React (including React Native). This skill encapsulates established testing philosophies such as Test-Driven Development (TDD) and Behavior-Driven Testing, and offers a toolbox of patterns and utilities to streamline and standardize your test code. It covers the use of test factories for generating mock data, custom render functions for component testing, and mocking strategies that help isolate units of code. The overarching principle is to ensure that all production code is written in response to a failing test, following the TDD red-green-refactor cycle.
Why Use It
Quality assurance is a critical component of any robust software development process. The patterns and utilities in this skill are designed to:
- Increase Confidence: Well-structured tests catch regressions and edge cases before they reach production.
- Support Maintainability: Test factories and reusable utilities reduce boilerplate and duplication, making tests easier to read and maintain.
- Enable TDD Workflows: By writing tests first, you define clear requirements and ensure that implementation aligns with intended behavior.
- Promote Good Testing Practices: Focusing tests on behavior rather than implementation details makes them more resilient to refactoring and less likely to break unnecessarily.
- Foster Collaboration: Descriptive test cases and standardized utilities make onboarding and code reviews smoother for teams.
How to Use It
1. Test-Driven Development (TDD)
TDD is a disciplined workflow:
- Write a Failing Test: Before writing any production code, express the desired behavior as a test case.
- Make It Pass: Write the smallest amount of code needed to make the test pass.
- Refactor: Clean up the code, both in the test and the implementation, while ensuring tests still pass.
Example:
// calculator.test.ts
it('adds two numbers', () => {
expect(add(2, 3)).toBe(5); // This test fails until "add" is implemented
});
// calculator.ts
export function add(a: number, b: number): number {
return a + b;
}
2. Behavior-Driven Testing
Focus on what your code should do, not how it does it. Write tests that describe visible outcomes and business requirements. Name test cases to clearly reflect expected behavior.
Example:
it('shows error message when login fails', () => {
// Simulate login failure and assert error message is visible
});
Avoid testing private methods or implementation details. Instead, interact with the system as a user (or consumer) would.
3. Factory Functions for Test Data
Use factory functions to generate mock data with sensible defaults. Factories enable you to DRY up your tests and make them more expressive.
Example factory function:
type User = { id: string; name: string; role: string };
export function getMockUser(overrides?: Partial<User>): User {
return {
id: 'default-id',
name: 'Test User',
role: 'user',
...overrides,
};
}
Usage in a test:
it('renders admin dashboard for admin user', () => {
const adminUser = getMockUser({ role: 'admin' });
// Pass adminUser to your component or function
});
4. Custom Render Utilities
When testing React (or React Native) components that depend on context providers (such as themes, routing, or state), create a custom render function that wraps the component with necessary providers.
Example:
// src/utils/testUtils.tsx
import { render } from '@testing-library/react-native';
import { ThemeProvider } from './theme';
export const renderWithTheme = (ui: React.ReactElement) => {
return render(
<ThemeProvider>{ui}</ThemeProvider>
);
};
Usage:
import { renderWithTheme } from 'utils/testUtils';
import { screen } from '@testing-library/react-native';
it('renders with theme', () => {
renderWithTheme(<MyComponent />);
expect(screen.getByText('Hello')).toBeTruthy();
});
5. Mocking Strategies
Use mocks to isolate the unit under test from its dependencies. Jest provides built-in facilities for mocking modules, functions, and timers.
Example:
jest.mock('api/userService', () => ({
fetchUser: jest.fn(() => Promise.resolve({ id: '1', name: 'Mock' }))
}));
This allows you to test how your code behaves under various scenarios without relying on real external services.
When to Use It
- Writing New Features: Start with tests to define the behavior, then implement the feature.
- Fixing Bugs: Write a failing test that reproduces the bug before fixing it.
- Refactoring: Ensure that existing behavior is preserved by relying on a comprehensive test suite.
- Developing Components: Use custom render utilities and test factories to make component tests concise and robust.
- Any Time You Need Repeatable or Isolated Tests: Factories and mocks help you generate consistent, isolated test scenarios.
Important Notes
- Never write production code without a failing test. This is the central tenet of TDD and enforces a strict workflow.
- Avoid over-mocking. Mock only what is necessary to isolate the unit under test.
- Test behavior, not implementation. Refactor-friendly tests focus on "what" the system does, not "how."
- Keep factory functions up to date as your data models evolve.
- Organize test utilities centrally so they are easily discoverable and reusable across your codebase.
By following these patterns and using the provided utilities, you will write tests that are effective, maintainable, and aligned with modern software engineering best practices.