Testing Fundamentals
Types of Tests: Unit, Integration, E2E
Understand when to write unit, integration, and end-to-end tests — and why the distinction matters.
Three Types of Tests
Not all tests are equal. Choosing the right type for each scenario determines whether your test suite is a valuable safety net or a slow, brittle burden.
Unit Tests
Unit tests verify a single function, class, or component in complete isolation. All external dependencies are replaced with mocks.
Characteristics: Very fast (milliseconds), very reliable, very cheap to write and maintain.
When to use: Business logic, utility functions, data transformations, algorithms, pure functions.
Integration Tests
Integration tests verify that multiple units work correctly together. They use real dependencies where possible — real database queries, real HTTP calls (to a test server), real file system.
Characteristics: Slower than unit tests (seconds), more realistic, catch interaction bugs.
When to use: API routes, database operations, service-to-service communication.
End-to-End Tests
E2E tests control a real browser and simulate real user actions against your running application.
Characteristics: Slow (seconds to minutes), most realistic, catch the bugs users would actually encounter.
When to use: Critical user flows: signup, login, checkout, core feature paths.
The Testing Trophy
Kent C. Dodds' "testing trophy" suggests writing MORE integration tests than the traditional pyramid suggests. Integration tests give the best return per test — more realistic than unit tests, faster than E2E.
Key Takeaways
- Unit tests verify isolated functions — fast, cheap, many of them
- Integration tests verify how components work together — more realistic, medium speed
- E2E tests verify complete user flows in a real browser — slowest, highest confidence, fewest needed
- The testing trophy: favor integration tests over unit tests where possible — better confidence per test
- Never test implementation details — test behavior (what the code does, not how it does it)
Example
// Three tests for the same feature at different levels
// Unit test — pure function in isolation
it('slugifies a title', () => {
expect(slugify('Hello World!')).toBe('hello-world');
});
// Integration test — API route with real database
it('creates a post with auto-generated slug', async () => {
const res = await POST({ body: { title: 'Hello World!' } });
const post = await res.json();
expect(post.slug).toBe('hello-world');
});
// E2E test — user creates a post in the browser
test('user creates and views a post', async ({ page }) => {
await page.goto('/posts/new');
await page.getByLabel('Title').fill('Hello World!');
await page.getByRole('button', { name: 'Publish' }).click();
await expect(page).toHaveURL('/posts/hello-world');
});// Three tests for the same feature at different levels
// Unit test — pure function in isolation
it('slugifies a title', () => {
expect(slugify('Hello World!')).toBe('hello-world');
});
// Integration test — API route with real database
it('creates a post with auto-generated slug', async () => {
const res = await POST({ body: { title: 'Hello World!' } });
const post = await res.json();
expect(post.slug).toBe('hello-world');
});
// E2E test — user creates a post in the browser
test('user creates and views a post', async ({ page }) => {
await page.goto('/posts/new');
await page.getByLabel('Title').fill('Hello World!');
await page.getByRole('button', { name: 'Publish' }).click();
await expect(page).toHaveURL('/posts/hello-world');
});