Skip to main content

Testing Strategy

Core Philosophy

Our testing strategy follows Kent C. Dodds' principle: "The more your tests resemble the way your software is used, the more confidence they can give you."

What We Test

Focus on testing behavior, not implementation. Test what users experience, not how the code achieves it.

// ❌ Testing implementation details
test('setState updates component state', () => {
const component = render(<UserProfile />);
expect(component.state.loading).toBe(true);
});

// ✅ Testing behavior
test('shows loading spinner while fetching user data', () => {
render(<UserProfile userId="123" />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});

The Testing Trophy

Prioritize tests based on confidence and maintenance cost:

       🏆 E2E Tests (10%)
/ \
/ \ Integration Tests (70%)
/ \
/________\ Unit Tests (20%)
  • Unit Tests: Pure functions, utilities, complex algorithms
  • Integration Tests: Component interactions, API calls, database operations
  • E2E Tests: Critical user journeys, payment flows, authentication

What NOT to Test

  1. Third-party libraries - Trust that Supabase, Next.js, etc. work
  2. Implementation details - Private methods, state structure, component internals
  3. Style/CSS - Unless it affects functionality
  4. Generated code - TypeScript types from Supabase
  5. Framework features - Next.js routing, React hooks

Testing Principles

1. Test User Behavior

// ❌ Testing internals
test('calls setNotes with API response', async () => {
const setNotes = jest.fn();
// ... testing that setNotes was called
});

// ✅ Testing behavior
test('displays notes after loading', async () => {
render(<NotesPage />);

await waitFor(() => {
expect(screen.getByText('My First Note')).toBeInTheDocument();
});
});

2. Avoid Excessive Mocking

// ❌ Over-mocking
jest.mock('@kit/supabase/server-client');
jest.mock('@kit/shared/logger');
jest.mock('next/navigation');

// ✅ Minimal mocking - only external services
const mockSupabase = {
from: () => ({
select: () => ({
eq: () => Promise.resolve({ data: mockNotes, error: null })
})
})
};

3. Use Real Data Structures

// ❌ Incomplete mocks
const mockUser = { id: '123' };

// ✅ Realistic test data
const mockUser: Tables<'users'> = {
id: '123',
email: 'test@example.com',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
// ... all required fields
};

4. Test Error Scenarios

test('shows error message when note creation fails', async () => {
// Simulate API failure
server.use(
http.post('/api/notes', () => {
return HttpResponse.json(
{ error: 'Insufficient permissions' },
{ status: 403 }
);
})
);

render(<CreateNoteForm />);

await userEvent.type(screen.getByLabelText('Title'), 'Test Note');
await userEvent.click(screen.getByText('Create'));

expect(await screen.findByText('Insufficient permissions')).toBeInTheDocument();
});

Testing by Feature Type

Server Components

// Test data fetching and error handling
test('NotesPage displays user notes', async () => {
const { container } = await render(
await NotesPage({ params: { userId: '123' } })
);

expect(container).toHaveTextContent('My Notes');
});

Server Actions

test('createNoteAction validates input and creates note', async () => {
const result = await createNoteAction({
title: 'Test',
content: 'Content'
});

expect(result.success).toBe(true);
expect(result.data.id).toBeDefined();
});

Client Components with Hooks

test('InteractiveNotes allows filtering', async () => {
render(<InteractiveNotes />);

const searchInput = screen.getByPlaceholderText('Search notes...');
await userEvent.type(searchInput, 'important');

expect(screen.getByText('Important Note')).toBeInTheDocument();
expect(screen.queryByText('Regular Note')).not.toBeInTheDocument();
});

Test Organization

Test Utilities

Create reusable test utilities:

// test-utils/render.tsx
export function renderWithProviders(
ui: React.ReactElement,
options?: RenderOptions
) {
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>,
options
);
}

// test-utils/factories.ts
export const createMockAccount = (
overrides?: Partial<Tables<'accounts'>>
): Tables<'accounts'> => ({
id: faker.string.uuid(),
name: faker.company.name(),
created_at: faker.date.past().toISOString(),
// ... defaults with overrides
});

Continuous Integration

Test Execution Strategy

# Run tests in parallel by type
test:
parallel:
matrix:
type: [unit, integration, e2e]
script:
- pnpm test:${{ matrix.type }}

Performance Optimization

  1. Parallel execution within test suites
  2. Shared test database for integration tests
  3. Selective test runs based on changed files
  4. Test result caching for unchanged code

Anti-Patterns to Avoid

1. Testing Framework Behavior

// ❌ Don't test React itself
test('useState updates state', () => {
// React's job, not yours
});

2. Snapshot Overuse

// ❌ Brittle snapshots
expect(component).toMatchSnapshot();

// ✅ Specific assertions
expect(screen.getByRole('heading')).toHaveTextContent('Dashboard');

3. Testing Private Functions

// ❌ Don't export just for testing
export const _privateHelper = () => {}; // exported for testing

// ✅ Test through public API
test('public method uses helper correctly', () => {
// Test the behavior, not the helper
});

4. Waiting for Arbitrary Timeouts

// ❌ Flaky time-based waits
await new Promise(resolve => setTimeout(resolve, 1000));

// ✅ Wait for specific conditions
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});

Decision Framework

When writing a test, ask:

  1. What would break if this fails? - User can't create notes? App crashes?
  2. Is this testing behavior or implementation? - Can I refactor without changing tests?
  3. Does this increase confidence? - Will this catch real bugs?
  4. Is the cost worth it? - Maintenance vs. value

If the answer to any of these suggests low value, reconsider the test.

Metrics That Matter

Track these instead of just coverage percentage:

  • Bug escape rate - Bugs found in production vs. testing
  • Test flakiness - Tests that fail intermittently
  • Test runtime - Fast feedback loops
  • Refactoring friction - How often tests break during refactoring

Remember: Quality over quantity. One good integration test is worth ten unit tests of implementation details.

Resources

  1. Write tests
  2. How to know what to test
  3. Confidently shipping code
  4. Testing implementation details