A Complete Guide to Testing in JavaScript
Testing is a fundamental aspect of professional software development. Tests ensure that your code works as intended, make maintenance easier, and help you catch regressions. This article explores the different approaches and tools for effectively testing your JavaScript applications.
Why Test Your Code?
- Confidence: Assurance that your code works correctly
- Documentation: Tests serve as living documentation
- Safe refactoring: Change code without fear of breaking features
- Quality: Fewer bugs and better design
- Collaboration: Easier teamwork
- Continuous deployment: Automated checks before deployment
Types of Tests
Unit Tests
Unit tests verify that the smallest units of code (functions, methods, classes) work correctly in isolation.
// Function to test
function somme(a, b) {
return a + b;
}
// Unit test with Jest
test('additionne correctement deux nombres', () => {
expect(somme(2, 3)).toBe(5);
expect(somme(-1, 1)).toBe(0);
expect(somme(0, 0)).toBe(0);
});
Integration Tests
Integration tests verify that different parts of the system work correctly together.
// Integration test with Jest
describe('Service utilisateur', () => {
test('crée un utilisateur et le récupère de la base de données', async () => {
// Create a user
const userId = await userService.createUser({
name: 'Alice',
email: 'alice@example.com'
});
// Retrieve the user
const user = await userService.getUserById(userId);
// Verify that the user is retrieved correctly
expect(user).toEqual({
id: userId,
name: 'Alice',
email: 'alice@example.com'
});
});
});
Functional / End-to-End (E2E) Tests
E2E tests verify the application’s behavior from the end user’s perspective.
// E2E test with Cypress
describe('Formulaire de connexion', () => {
it('permet à un utilisateur de se connecter', () => {
cy.visit('/login');
cy.get('input[name="email"]').type('utilisateur@exemple.fr');
cy.get('input[name="password"]').type('motdepasse123');
cy.get('button[type="submit"]').click();
// Verify that the user is redirected to the dashboard
cy.url().should('include', '/dashboard');
cy.contains('Bienvenue, Utilisateur').should('be.visible');
});
});
Component Tests
For component-based applications (React, Vue, Angular), component tests verify the rendering and behavior of UI components.
// React component test with React Testing Library
import { render, screen, fireEvent } from '@testing-library/react';
import Compteur from './Compteur';
test('incrémente le compteur lorsque le bouton est cliqué', () => {
render(<Compteur />);
// Verify the initial state
expect(screen.getByText('Compteur: 0')).toBeInTheDocument();
// Click the button
fireEvent.click(screen.getByText('Incrémenter'));
// Verify that the counter was incremented
expect(screen.getByText('Compteur: 1')).toBeInTheDocument();
});
Performance Tests
Performance tests verify that your application meets speed and efficiency requirements.
// Performance test with Lighthouse CI
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/'],
numberOfRuns: 3,
},
assert: {
assertions: {
'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
'interactive': ['error', { maxNumericValue: 3000 }],
'max-potential-fid': ['error', { maxNumericValue: 100 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
Security Tests
Security tests identify potential vulnerabilities in your application.
// Example with OWASP ZAP (via CLI)
// zap-cli quick-scan --self-contained --start-options "-config api.disablekey=true" https://example.com
Popular Testing Frameworks and Tools
Jest
Jest is a complete JavaScript testing framework that requires minimal configuration.
// Installation
// npm install --save-dev jest
// Minimal configuration (package.json)
{
"scripts": {
"test": "jest"
}
}
// Example test
const { somme } = require('./math');
describe('Fonctions mathématiques', () => {
test('somme additionne deux nombres', () => {
expect(somme(1, 2)).toBe(3);
});
test('somme gère les nombres négatifs', () => {
expect(somme(-1, -2)).toBe(-3);
});
test('somme avec zéro retourne l\'autre nombre', () => {
expect(somme(0, 5)).toBe(5);
});
});
Mocha + Chai
Mocha is a flexible testing framework, often used with Chai for assertions.
// Installation
// npm install --save-dev mocha chai
// Example test
const { expect } = require('chai');
const { somme } = require('./math');
describe('Fonctions mathématiques', () => {
it('somme additionne deux nombres', () => {
expect(somme(1, 2)).to.equal(3);
});
it('somme gère les nombres négatifs', () => {
expect(somme(-1, -2)).to.equal(-3);
});
});
Cypress
Cypress is a modern E2E testing tool that runs in the browser.
// Installation
// npm install --save-dev cypress
// Example test
describe('Page d\'accueil', () => {
beforeEach(() => {
cy.visit('/');
});
it('affiche le titre correct', () => {
cy.get('h1').should('contain', 'Bienvenue sur notre site');
});
it('navigue vers la page À propos', () => {
cy.get('nav').contains('À propos').click();
cy.url().should('include', '/about');
cy.get('h1').should('contain', 'À propos de nous');
});
});
Playwright
Playwright is an E2E testing tool that supports multiple browsers.
// Installation
// npm install --save-dev @playwright/test
// Example test
const { test, expect } = require('@playwright/test');
test('page d\'accueil a le bon titre', async ({ page }) => {
await page.goto('https://example.com');
// Verify the title
const title = await page.title();
expect(title).toBe('Example Domain');
// Verify the content
const heading = await page.textContent('h1');
expect(heading).toBe('Example Domain');
});
Testing Library
Testing Library is a family of libraries for testing user interfaces in a user-centric way.
// Installation for React
// npm install --save-dev @testing-library/react @testing-library/jest-dom
// Example test
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Formulaire from './Formulaire';
test('soumet le formulaire avec les valeurs correctes', () => {
const handleSubmit = jest.fn();
render(<Formulaire onSubmit={handleSubmit} />);
// Fill out the form
fireEvent.change(screen.getByLabelText(/nom/i), {
target: { value: 'Alice' },
});
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'alice@example.com' },
});
// Submit the form
fireEvent.click(screen.getByRole('button', { name: /soumettre/i }));
// Verify that the onSubmit function was called with the correct values
expect(handleSubmit).toHaveBeenCalledWith({
nom: 'Alice',
email: 'alice@example.com',
});
});
Storybook
Storybook lets you develop and test UI components in isolation.
// Installation
// npx sb init
// Example story
// Button.stories.js
import React from 'react';
import { Button } from './Button';
export default {
title: 'Components/Button',
component: Button,
argTypes: {
backgroundColor: { control: 'color' },
},
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
label: 'Button',
};
Testing Approaches
Test-Driven Development (TDD)
TDD follows a three-step cycle:
- Red: Write a failing test
- Green: Write the minimum code needed to make the test pass
- Refactor: Improve the code without changing its behavior
// TDD example
// 1. Red: Write a failing test
test('convertit les degrés Celsius en Fahrenheit', () => {
expect(celsiusToFahrenheit(0)).toBe(32);
expect(celsiusToFahrenheit(100)).toBe(212);
});
// 2. Green: Write the minimum code to make the test pass
function celsiusToFahrenheit(celsius) {
return celsius * 9/5 + 32;
}
// 3. Refactor: Improve the code (if necessary)
function celsiusToFahrenheit(celsius) {
const factor = 9/5;
const freezingPointDiff = 32;
return celsius * factor + freezingPointDiff;
}
Behavior-Driven Development (BDD)
BDD focuses on the system’s expected behavior from the user’s perspective.
// Example with Cucumber.js
// Feature: Connexion utilisateur
// En tant qu'utilisateur enregistré
// Je veux pouvoir me connecter
// Afin d'accéder à mon compte
// Scénario: Connexion réussie
// Étant donné que je suis sur la page de connexion
// Quand je saisis mon email "utilisateur@exemple.fr"
// Et je saisis mon mot de passe "motdepasse123"
// Et je clique sur le bouton "Se connecter"
// Alors je devrais être redirigé vers la page "Tableau de bord"
// Et je devrais voir "Bienvenue, Utilisateur"
// Step implementation
const { Given, When, Then } = require('@cucumber/cucumber');
Given('je suis sur la page de connexion', async function() {
await this.navigateTo('/login');
});
When('je saisis mon email {string}', async function(email) {
await this.fillField('email', email);
});
// ... other steps
Mocks, Stubs, and Spies
Mocks
Mocks replace real dependencies with simulated versions.
// Example with Jest
test('envoie un email de bienvenue lors de l\'inscription', () => {
// Mock of the email service
const emailService = {
sendWelcomeEmail: jest.fn()
};
// System under test
const userService = new UserService(emailService);
// Action
userService.registerUser('alice@example.com', 'password123');
// Assertion
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith('alice@example.com');
});
Stubs
Stubs provide predefined responses to function calls.
// Example with Sinon.js
const sinon = require('sinon');
const database = require('./database');
test('récupère les données utilisateur', async () => {
// Create a stub for the query method
const queryStub = sinon.stub(database, 'query');
// Configure the stub to return specific data
queryStub.withArgs('SELECT * FROM users WHERE id = ?', [42])
.resolves([{ id: 42, name: 'Alice' }]);
// System under test
const userService = new UserService(database);
// Action
const user = await userService.getUserById(42);
// Assertion
expect(user).toEqual({ id: 42, name: 'Alice' });
// Restore the stub
queryStub.restore();
});
Spies
Spies observe function calls without changing their behavior.
// Example with Jest
test('enregistre les erreurs dans la console', () => {
// Create a spy on console.error
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
// Action that should generate an error
const calculator = new Calculator();
calculator.divide(10, 0);
// Assertion
expect(errorSpy).toHaveBeenCalledWith('Division par zéro');
// Restore the spy
errorSpy.mockRestore();
});
Asynchronous Tests
Promises
// Test with Jest
test('récupère les données utilisateur', () => {
return userService.getUserById(42)
.then(user => {
expect(user.name).toBe('Alice');
});
});
// Or with async/await
test('récupère les données utilisateur', async () => {
const user = await userService.getUserById(42);
expect(user.name).toBe('Alice');
});
Callbacks
// Test with Jest
test('lit un fichier', done => {
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
done(err);
return;
}
try {
expect(data).toContain('Hello, world');
done();
} catch (error) {
done(error);
}
});
});
Code Coverage
Code coverage measures how much of your code is executed during tests.
// Jest configuration for coverage
// package.json
{
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.{js,jsx}",
"!**/node_modules/**",
"!**/vendor/**"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
Continuous Integration (CI)
Continuous integration automatically runs your tests on every code change.
# Example with GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v2
Best Practices
- Test critical features first: Focus on the most important code
- Keep tests simple and readable: A test should be easy to understand
- One test, one assertion: Each test should verify a single thing
- Isolate your tests: Tests should not depend on one another
- Use realistic test data: Avoid overly simplistic data
- Avoid brittle tests: Tests should not break for superficial reasons
- Run tests regularly: Ideally on every code change
- Maintain your tests: Tests are code and require maintenance
Common Pitfalls to Avoid
- Testing implementation instead of behavior: Focus on what the code does, not how it does it
- Ignoring edge cases: Test extreme values and error cases
- Testing only the happy path: Don’t forget to test failure cases
- Tests too tightly coupled to the code: Overly specific tests break easily during refactoring
- Neglecting regression tests: Add tests for bugs you’ve fixed
- Slow tests: Slow tests discourage running them frequently
Conclusion
Testing is an investment that pays dividends throughout the life of a project. It improves code quality, makes maintenance easier, and enables more frequent and more reliable deployments.
There is no one-size-fits-all approach to testing. The optimal strategy depends on the nature of your project, your team, and your constraints. What matters is to start somewhere and gradually improve your test coverage.
By building a testing culture in your team and following the best practices presented in this article, you can significantly improve the quality and maintainability of your JavaScript applications.
About InSkillCoach
Training and technology expert
Coach specializing in advanced technologies and AI, backed by GNeurone Inc.
Certifications:
- AWS Certified Solutions Architect – Professional
- Google Cloud certifications
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comments
Comments are powered by GitHub Discussions
Sign in with GitHub to join the discussion