Testing in JavaScript: from unit to end-to-end tests
\n\nQuality testing is an integral part of professional JavaScript development. In this article, we will cover three main levels of testing: unit, integration, and e2e tests. You will learn which tools to use, how to write tests in Node.js, and how to build an effective testing strategy.
\n\n1. Unit Testing: the foundation of the testing pyramid
\n\nUnit tests check isolated modules — functions, classes, or individual methods. The main rule: the code being tested should not depend on external resources (databases, APIs, file system).
\n\nTools for unit testing
\n- \n
- Jest — the most popular framework from Facebook. It includes mocks, snapshots, and code coverage out of the box. \n
- Mocha + Chai — a flexible combo for those who like to configure everything manually. \n
- Vitest — a modern alternative to Jest, optimized for Vite projects. \n
// Example unit test with Jest\nconst { calculateDiscount } = require('./pricing');\n\ndescribe('calculateDiscount', () => {\n test('returns 10% discount for amounts > 1000', () => {\n const result = calculateDiscount(1500);\n expect(result).toBe(1350);\n });\n\n test('does not apply discount for amounts <= 1000', () => {\n const result = calculateDiscount(500);\n expect(result).toBe(500);\n });\n\n test('handles boundary value 1000', () => {\n const result = calculateDiscount(1000);\n expect(result).toBe(1000);\n });\n});\n\nBest practices for unit testing
\n- \n
- Test one behavior at a time (one assert per test). \n
- Use descriptive test names (given/when/then). \n
- Avoid testing private methods — test the public interface. \n
- Maintain code coverage at 70-80%. \n
2. Integration Testing: checking component interaction
\n\nIntegration tests check how different modules work together. In the context of Node.js, this usually means testing with a real database, file system, or external APIs.
\n\nTypical integration testing scenarios
\n- \n
- Interaction between the controller, service layer, and database. \n
- Testing middleware and Express/Koa routes. \n
- Testing repositories with a test database (SQLite in memory or PostgreSQL in Docker). \n
// Example integration test with Supertest and Jest\nconst request = require('supertest');\nconst app = require('../app');\nconst { createTestUser, cleanDatabase } = require('./helpers');\n\nbeforeEach(async () => {\n await cleanDatabase();\n});\n\ndescribe('POST /api/users', () => {\n test('creates a user and returns 201', async () => {\n const response = await request(app)\n .post('/api/users')\n .send({ name: 'Ivan', email: 'ivan@test.com' });\n\n expect(response.status).toBe(201);\n expect(response.body).toHaveProperty('id');\n expect(response.body.name).toBe('Ivan');\n });\n\n test('returns 400 on duplicate email', async () => {\n await createTestUser({ email: 'ivan@test.com' });\n\n const response = await request(app)\n .post('/api/users')\n .send({ name: 'Petr', email: 'ivan@test.com' });\n\n expect(response.status).toBe(400);\n expect(response.body.error).toBe('Email is already in use');\n });\n});\n\nSetting up test infrastructure
\n- \n
- Docker Compose — for spinning up test services (DB, Redis). \n
- Testcontainers — programmatic container management from tests. \n
- In-memory databases (e.g., mongodb-memory-server) — for fast tests without external dependencies. \n