0%
A Complete Guide to Testing in JavaScript

A Complete Guide to Testing in JavaScript

Strategies and tools for effectively testing your JavaScript applications and ensuring their quality.

I

InSkillCoach

· min

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

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:

  1. Red: Write a failing test
  2. Green: Write the minimum code needed to make the test pass
  3. 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

  1. Test critical features first: Focus on the most important code
  2. Keep tests simple and readable: A test should be easy to understand
  3. One test, one assertion: Each test should verify a single thing
  4. Isolate your tests: Tests should not depend on one another
  5. Use realistic test data: Avoid overly simplistic data
  6. Avoid brittle tests: Tests should not break for superficial reasons
  7. Run tests regularly: Ideally on every code change
  8. Maintain your tests: Tests are code and require maintenance

Common Pitfalls to Avoid

  1. Testing implementation instead of behavior: Focus on what the code does, not how it does it
  2. Ignoring edge cases: Test extreme values and error cases
  3. Testing only the happy path: Don’t forget to test failure cases
  4. Tests too tightly coupled to the code: Overly specific tests break easily during refactoring
  5. Neglecting regression tests: Add tests for bugs you’ve fixed
  6. 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.

InSkillCoach

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+
1.6k
65

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!