Complete Testing Strategy with Jest, Mocha & Cypress
This comprehensive project demonstrates a complete full-stack testing strategy using three different testing frameworks: Jest for frontend unit tests, Mocha for backend API tests, and Cypress for end-to-end testing.
By combining these three frameworks, we achieve complete test coverage across all application layers, following the testing pyramid principle. Each framework is used where it excels most, providing fast, reliable, and maintainable test suites.
Following the testing pyramid principle for optimal coverage and speed
Testing React components, hooks, and utility functions with Jest and React Testing Library:
// TaskItem.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import TaskItem from './TaskItem';
describe('TaskItem Component', () => {
const mockTask = {
id: 1,
title: 'Complete project',
completed: false
};
it('should render task title', () => {
render(<TaskItem task={mockTask} />);
expect(screen.getByText('Complete project')).toBeInTheDocument();
});
it('should toggle task completion on click', () => {
const onToggle = jest.fn();
render(<TaskItem task={mockTask} onToggle={onToggle} />);
const checkbox = screen.getByRole('checkbox');
fireEvent.click(checkbox);
expect(onToggle).toHaveBeenCalledWith(1);
});
it('should apply completed style when task is done', () => {
const completedTask = { ...mockTask, completed: true };
render(<TaskItem task={completedTask} />);
const title = screen.getByText('Complete project');
expect(title).toHaveClass('task-completed');
});
});
// useTasks.test.js
import { renderHook, act } from '@testing-library/react';
import useTasks from './useTasks';
describe('useTasks Hook', () => {
it('should add new task', () => {
const { result } = renderHook(() => useTasks());
act(() => {
result.current.addTask('New Task');
});
expect(result.current.tasks).toHaveLength(1);
expect(result.current.tasks[0].title).toBe('New Task');
});
it('should filter completed tasks', () => {
const { result } = renderHook(() => useTasks());
act(() => {
result.current.addTask('Task 1');
result.current.addTask('Task 2');
result.current.toggleTask(result.current.tasks[0].id);
});
const completed = result.current.getCompletedTasks();
expect(completed).toHaveLength(1);
});
});
Testing RESTful API endpoints with Mocha, Chai, and Supertest:
// tasks.api.test.js
const { expect } = require('chai');
const request = require('supertest');
const app = require('../app');
describe('Tasks API', function() {
describe('GET /api/tasks', function() {
it('should return all tasks', async function() {
const response = await request(app)
.get('/api/tasks')
.expect(200);
expect(response.body).to.be.an('array');
expect(response.body).to.have.lengthOf.at.least(0);
});
it('should filter tasks by status', async function() {
const response = await request(app)
.get('/api/tasks?status=completed')
.expect(200);
expect(response.body.every(task =>
task.completed === true
)).to.be.true;
});
});
describe('POST /api/tasks', function() {
it('should create a new task', async function() {
const newTask = {
title: 'Test Task',
description: 'Test Description'
};
const response = await request(app)
.post('/api/tasks')
.send(newTask)
.expect(201);
expect(response.body).to.include(newTask);
expect(response.body).to.have.property('id');
expect(response.body.completed).to.be.false;
});
it('should validate required fields', async function() {
const response = await request(app)
.post('/api/tasks')
.send({})
.expect(400);
expect(response.body.error).to.match(/title.*required/i);
});
});
});
// database.test.js
const { expect } = require('chai');
const TaskRepository = require('../repositories/TaskRepository');
describe('Task Repository', function() {
beforeEach(async function() {
await TaskRepository.deleteAll();
});
it('should save and retrieve task', async function() {
const task = await TaskRepository.create({
title: 'Database Test'
});
const retrieved = await TaskRepository.findById(task.id);
expect(retrieved.title).to.equal('Database Test');
});
it('should update task status', async function() {
const task = await TaskRepository.create({
title: 'Update Test'
});
await TaskRepository.update(task.id, { completed: true });
const updated = await TaskRepository.findById(task.id);
expect(updated.completed).to.be.true;
});
});
Testing complete user flows and cross-component integration:
// tasks.e2e.cy.js
describe('Task Management Flow', () => {
beforeEach(() => {
cy.visit('/');
});
it('should complete full task lifecycle', () => {
// Create new task
cy.get('[data-cy="task-input"]')
.type('Buy groceries');
cy.get('[data-cy="add-task-btn"]')
.click();
// Verify task appears
cy.get('[data-cy="task-list"]')
.should('contain', 'Buy groceries');
// Mark as complete
cy.get('[data-cy="task-item"]')
.first()
.find('[data-cy="checkbox"]')
.click();
// Verify completed state
cy.get('[data-cy="task-item"]')
.first()
.should('have.class', 'completed');
// Delete task
cy.get('[data-cy="delete-btn"]')
.first()
.click();
cy.get('[data-cy="confirm-delete"]')
.click();
// Verify task removed
cy.get('[data-cy="task-list"]')
.should('not.contain', 'Buy groceries');
});
it('should filter tasks by status', () => {
// Add multiple tasks
const tasks = ['Task 1', 'Task 2', 'Task 3'];
tasks.forEach(task => {
cy.get('[data-cy="task-input"]').type(task);
cy.get('[data-cy="add-task-btn"]').click();
});
// Complete one task
cy.get('[data-cy="task-item"]')
.first()
.find('[data-cy="checkbox"]')
.click();
// Filter to show only active
cy.get('[data-cy="filter-active"]').click();
cy.get('[data-cy="task-item"]').should('have.length', 2);
// Filter to show completed
cy.get('[data-cy="filter-completed"]').click();
cy.get('[data-cy="task-item"]').should('have.length', 1);
});
});
// api-stubbing.cy.js
describe('Task API Integration', () => {
it('should handle API errors gracefully', () => {
// Stub API to return error
cy.intercept('POST', '/api/tasks', {
statusCode: 500,
body: { error: 'Server error' }
}).as('createTask');
cy.visit('/');
cy.get('[data-cy="task-input"]').type('Test Task');
cy.get('[data-cy="add-task-btn"]').click();
cy.wait('@createTask');
// Verify error message shown
cy.get('[data-cy="error-message"]')
.should('be.visible')
.and('contain', 'Failed to create task');
});
it('should load tasks from API', () => {
const mockTasks = [
{ id: 1, title: 'Mock Task 1', completed: false },
{ id: 2, title: 'Mock Task 2', completed: true }
];
cy.intercept('GET', '/api/tasks', mockTasks).as('getTasks');
cy.visit('/');
cy.wait('@getTasks');
cy.get('[data-cy="task-item"]').should('have.length', 2);
cy.get('[data-cy="task-list"]').should('contain', 'Mock Task 1');
});
});
Unit tests (Jest) run in milliseconds, providing instant feedback during development. Integration tests (Mocha) run in seconds. E2E tests (Cypress) run slower but catch integration issues.
Each framework excels in its domain: Jest for React components, Mocha for backend APIs with flexible assertions, Cypress for real user interactions.
Combined coverage across all layers ensures no gaps. Unit tests catch logic errors, integration tests catch API issues, E2E tests catch UX problems.
Fast unit tests run on every commit, integration tests on pull requests, E2E tests before deployment. Optimized CI/CD pipeline.
Develop new feature or fix bug
Test individual components - runs in <2s
Test API endpoints - runs in <5s
Test critical user flows before deploy
{
"scripts": {
"test": "npm run test:unit && npm run test:integration && npm run test:e2e",
"test:unit": "jest --coverage",
"test:unit:watch": "jest --watch",
"test:integration": "mocha test/integration/**/*.test.js",
"test:integration:watch": "mocha --watch test/integration/**/*.test.js",
"test:e2e": "cypress run",
"test:e2e:open": "cypress open",
"test:ci": "npm run test:unit && npm run test:integration"
}
}
Try out the application that's being tested with all three frameworks:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy',
},
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/index.js',
'!src/reportWebVitals.js',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
// .mocharc.json
{
"require": ["./test/setup.js"],
"spec": "test/integration/**/*.test.js",
"timeout": 5000,
"reporter": "spec",
"ui": "bdd",
"exit": true
}
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: true,
screenshotOnRunFailure: true,
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});
name: Full-Stack Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run Unit Tests (Jest)
run: npm run test:unit
- name: Run Integration Tests (Mocha)
run: npm run test:integration
- name: Run E2E Tests (Cypress)
uses: cypress-io/github-action@v5
with:
start: npm start
wait-on: 'http://localhost:3000'
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
- name: Upload Cypress Videos
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-videos
path: cypress/videos
Understanding when to use unit, integration, and E2E tests. Each layer serves a specific purpose in the testing pyramid.
Fast feedback loop with unit tests, slower but thorough E2E tests. Balance between speed and confidence.
Deep knowledge of Jest for React, Mocha for backend APIs, and Cypress for E2E. Each framework's strengths and trade-offs.
Achieving 97% code coverage across all layers. Understanding meaningful coverage vs vanity metrics.
Automated testing in pipelines. Fast tests on commits, full suite before deployment. Continuous quality assurance.
Test isolation, mocking strategies, data-driven tests, and maintainable test code. Writing tests that add value.
| Framework | Test Count | Coverage | Execution Time | Purpose |
|---|---|---|---|---|
| Jest | 70 tests | 95% | <2s | Frontend components & logic |
| Mocha | 42 tests | 98% | ~3s | Backend API & database |
| Cypress | 15 tests | N/A | ~15s | End-to-end user flows |
| TOTAL | 127 tests | 97% | <5s | Full-stack coverage |
This full-stack testing suite demonstrates a production-ready testing strategy that combines the strengths of three powerful frameworks. By using Jest for fast unit tests, Mocha for flexible API testing, and Cypress for comprehensive E2E testing, we achieve optimal coverage with efficient execution times.
The key to successful testing isn't using every tool availableโit's using the right tool for each job. This project showcases that understanding, providing a blueprint for scalable, maintainable test suites in modern full-stack applications.