A Todo app demonstrating three-tier testing with 100% merged coverage using nextcov.
This is a todo-app using React, Next.js and TypeScript. The idea is mostly from this article, and the code is referenced from here.
Key Highlights:
- Three-tier testing: Unit + Component + Integration (100% merged coverage)
- V8 code coverage collection using nextcov
- Coverage merging with statement normalization
- json-server as mock server for SSR tests
- GitHub Actions CI pipeline
Constraint: The current solution for SSR test is by changing the mock server data before each test, which means tests can only be run serially. Parallel testing is not supported at the moment.
# Install dependencies
npm install
# Start mock server (in one terminal)
npm run mock
# Start development server (in another terminal)
npm run devOpen http://localhost:3000 with your browser to see the result.
This project uses a three-tier testing strategy with Vitest for unit and component tests and Playwright for integration tests, with coverage collection and merging powered by nextcov.
| Test Type | Environment | Best For | Coverage Focus |
|---|---|---|---|
| Unit | jsdom | Utilities, hooks, logic | Business logic, validation |
| Component | Real browser (Playwright) | UI components, interactions | React components, browser APIs |
| Integration | Full Next.js server | User flows, pages | Server components, SSR |
Fast unit tests running in jsdom environment for utilities and hooks.
# Run unit tests with coverage
npm run test:unit
# Run tests in watch mode
npm run test:watch
# Run with Vitest UI dashboard
npm run test:unit:uiCoverage output: ./coverage/unit
View report: npm run lcov:unit
Real browser tests using Vitest browser mode with Playwright. Components render in an actual browser with real DOM APIs, CSS, and user interactions.
# Run component tests (headless)
npm run test:component
# Run with visible browser for debugging
npm run test:component:headedCoverage output: ./coverage/component
View report: npm run lcov:component
Full end-to-end tests using Playwright that test the complete application including React Server Components and SSR.
# Run integration tests (local)
# 1. Build for local testing
npm run build:local
# 2. Start mock server and Next.js server
npm run start:integration
# 3. Run Playwright tests (in another terminal)
npm run playwright-test
# Or run all-in-one (CI mode)
npm run integration-testCoverage output: ./coverage/integration
View report: npm run lcov:integration
This project uses nextcov to collect V8 coverage from all three test types and merge them into a unified report.
# Run all tests (unit + component)
npm test
# Run integration tests (see steps above)
# Merge all coverage sources into a single report
npm run coverage:mergeCoverage Output:
- Unit test coverage:
coverage/unit/ - Component test coverage:
coverage/component/ - Integration test coverage:
coverage/integration/ - Merged coverage:
coverage/merged/(HTML report atcoverage/merged/index.html)
View merged report: npm run lcov:merged
Coverage by Test Type:
| Test Type | Overall % | What It Covers |
|---|---|---|
| Unit Tests | ~36% | Utilities, hooks |
| Component Tests | ~52% | UI components (TodoList, TodoItem) |
| Integration Tests | ~88% | Pages, server components, SSR |
| Merged | 100% | Complete picture of your application |
Why Three Test Types?
Each test type excels at different aspects:
- Unit tests - Fast, isolated tests for business logic that doesn't need a browser
- Component tests - Real browser rendering for components that use browser APIs (localStorage, CSS, events)
- Integration tests - Full integration testing including React Server Components which can't be unit tested
Before merging, the coverage:strip script removes import statements and Next.js directives from unit/component coverage. This normalizes statement counts between environments:
- jsdom doesn't count imports as statements
- Real browser counts imports as executable statements
- Next.js bundled has different statement structure
The strip script ensures apples-to-apples comparison when merging.
npm run dev- Start Next.js development servernpm run build- Build for productionnpm run start- Start production servernpm run mock- Start json-server mock API
npm test- Run unit + component tests with coveragenpm run test:unit- Run unit tests only (jsdom)npm run test:unit:ui- Run unit tests with Vitest UI dashboardnpm run test:watch- Run unit tests in watch modenpm run test:component- Run component tests (browser)npm run test:component:headed- Run component tests with visible browser
npm run build:local- Build with E2E mode enablednpm run start:integration- Start Next.js + mock servernpm run playwright-test- Run Playwright testsnpm run integration-test- All-in-one integration test (CI)
npm run coverage:strip- Normalize coverage datanpm run coverage:merge- Merge all coverage sourcesnpm run lcov:unit- Open unit coverage reportnpm run lcov:component- Open component coverage reportnpm run lcov:integration- Open integration coverage reportnpm run lcov:merged- Open merged coverage report
- Vitest: Test runner for unit and browser tests
- @vitest/browser: Browser testing with Playwright provider
- Playwright: Integration testing and browser automation
- nextcov: V8 coverage collection for Next.js with Playwright
- @vitest/coverage-v8: V8 coverage for Vitest tests
- json-server: Mock REST API for SSR testing
The GitHub Actions workflow runs all tests and merges coverage:
- Run unit tests (
npm run test:unit) - Run component tests (
npm run test:component) - Build for integration testing (
npm run build:local) - Run integration tests (
npm run integration-test) - Merge all coverage (
npm run coverage:merge)
See .github/workflows/ci.yml for details.