Advanced JavaScript Testing: Unit, Integration, E2E

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Advanced Testing is a Must-Have for JavaScript Projects

In modern Node.js development, testing is no longer an optional stage. Without quality code coverage by tests, any refactoring or adding new functionality risks turning into "Russian roulette." However, simply writing a few asserts is not enough. At an advanced level, a developer must master three main layers: Unit tests (checking isolated modules), Integration tests (interaction of components, databases, APIs), and E2E tests (simulating user actions). In this article, we will analyze practical techniques for each layer using Node.js with modern tools: Jest, Supertest, and Playwright.



1. Unit Testing: Isolation, Mocks, and Stubs at an Advanced Level

Unit tests check the operation of a single function or module in isolation from external dependencies (database, file system, network requests). The key task is to correctly mock these dependencies so that the test is fast and deterministic.



Advanced Mocking Techniques in Jest

Consider an example: a function that accesses an external API and caches the result in Redis. We need to test the caching logic without making real requests.

// cacheService.jsconst axios = require('axios');const redis = require('redis');

class CacheService { constructor() { this.client = redis.createClient(); }

async fetchData(url) { const cached = await this.client.get(url); if (cached) return JSON.parse(cached);

const response = await axios.get(url); await this.client.setEx(url, 3600, JSON.stringify(response.data)); return response.data; }}

module.exports = CacheService;


// cacheService.test.jsconst CacheService = require('./cacheService');const axios = require('axios');const redis = require('redis');

jest.mock('axios');jest.mock('redis', () => ({ createClient: jest.fn(() => ({ get: jest.fn(), setEx: jest.fn() }))}));

describe('CacheService', () => { let service; let mockRedisClient;

beforeEach(() => { mockRedisClient = redis.createClient(); service = new CacheService(); });

test('should return cached data if present', async () => { const url = 'https://api.example.com/data'; const cachedData = { id: 1, value: 'cached' }; mockRedisClient.get.mockResolvedValue(JSON.stringify(cachedData));

const result = await service.fetchData(url);

expect(result).toEqual(cachedData); expect(axios.get).not.toHaveBeenCalled(); });

test('should fetch and cache data if not cached', async () => { const url = 'https://api.example.com/data'; const apiData = { id: 2, value: 'fresh' }; mockRedisClient.get.mockResolvedValue(null); axios.get.mockResolvedValue({ data: apiData });

const result = await service.fetchData(url);

expect(result).toEqual(apiData); expect(mockRedisClient.setEx).toHaveBeenCalledWith(url, 3600, JSON.stringify(apiData)); });});

Key points:

  • jest.mock('axios') automatically mocks the entire axios module.
  • For Redis, we use a factory that returns a mock with get and setEx methods.
  • We verify that when the cache is present, the external call does not occur.


2. Integration Tests: Testing Interaction with a Real Database and API

Integration tests check how different parts of the system work together. For Node.js, this often means testing Express routes with a real (or test) database. The best tool is Supertest paired with Jest.



Setting Up a Test Database and Supertest

Example: An API for managing users. We use in-memory SQLite for isolation

Blogs

Book Recommendations