Tests JavaScript avec Jest et Mocha

Guide d'utilisation de Jest et Mocha pour tester des applications JavaScript.

Tests avec Jest

// Installation
npm install --save-dev jest

// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

// math.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;

// math.test.js
import { add, multiply } from './math';

describe('Math functions', () => {
  test('adds two numbers correctly', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
    expect(add(0, 0)).toBe(0);
  });

  test('multiplies two numbers correctly', () => {
    expect(multiply(2, 3)).toBe(6);
    expect(multiply(-2, 3)).toBe(-6);
    expect(multiply(0, 5)).toBe(0);
  });
});

// async.test.js
test('async function', async () => {
  const data = await fetchData();
  expect(data).toBeDefined();
});

// mock.test.js
jest.mock('./api');
import { fetchUser } from './api';

test('mocking API call', async () => {
  fetchUser.mockResolvedValue({ id: 1, name: 'John' });

  const user = await fetchUser(1);
  expect(user.name).toBe('John');
  expect(fetchUser).toHaveBeenCalledWith(1);
});

Tests avec Mocha

// Installation
npm install --save-dev mocha chai

// test/math.test.js
const { expect } = require('chai');
const { add, multiply } = require('../src/math');

describe('Math functions', () => {
  describe('add()', () => {
    it('should add two positive numbers', () => {
      expect(add(2, 3)).to.equal(5);
    });

    it('should handle negative numbers', () => {
      expect(add(-1, 1)).to.equal(0);
    });

    it('should handle zero', () => {
      expect(add(0, 0)).to.equal(0);
    });
  });

  describe('multiply()', () => {
    it('should multiply two numbers', () => {
      expect(multiply(2, 3)).to.equal(6);
    });

    it('should handle negative numbers', () => {
      expect(multiply(-2, 3)).to.equal(-6);
    });
  });
});

// test/async.test.js
const { fetchData } = require('../src/api');

describe('Async operations', () => {
  it('should fetch data', async () => {
    const data = await fetchData();
    expect(data).to.exist;
  });

  it('should handle errors', async () => {
    try {
      await fetchData('invalid');
      expect.fail('Should have thrown');
    } catch (error) {
      expect(error).to.exist;
    }
  });
});

// test/hooks.test.js
describe('Database operations', () => {
  before(async () => {
    // Setup database connection
    await db.connect();
  });

  beforeEach(async () => {
    // Clean data before each test
    await db.clear();
  });

  after(async () => {
    // Close database connection
    await db.disconnect();
  });

  it('should save user', async () => {
    const user = { name: 'John' };
    await db.users.save(user);
    const saved = await db.users.findOne({ name: 'John' });
    expect(saved).to.deep.equal(user);
  });
});

Configuration

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  transform: {
    '^.+\\.jsx?$': 'babel-jest'
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

// .mocharc.js
module.exports = {
  require: ['@babel/register', './test/setup.js'],
  reporter: 'spec',
  timeout: 5000,
  file: ['./test/setup.js']
};

Commandes courantes :

# Jest
npm test               # Exécuter les tests
npm test -- --watch   # Mode watch
npm test -- --coverage # Rapport de couverture

# Mocha
mocha                 # Exécuter les tests
mocha --watch        # Mode watch
mocha --reporter dot # Changer le format de sortie

Tests de composants React

// Button.test.jsx
import { render, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button component', () => {
  test('renders with text', () => {
    const { getByText } = render(
      <Button>Click me</Button>
    );
    expect(getByText('Click me')).toBeInTheDocument();
  });

  test('calls onClick when clicked', () => {
    const onClick = jest.fn();
    const { getByText } = render(
      <Button onClick={onClick}>Click me</Button>
    );

    fireEvent.click(getByText('Click me'));
    expect(onClick).toHaveBeenCalled();
  });
});

// Form.test.jsx
import { render, fireEvent, waitFor } from '@testing-library/react';
import Form from './Form';

describe('Form component', () => {
  test('submits form data', async () => {
    const onSubmit = jest.fn();
    const { getByLabelText, getByText } = render(
      <Form onSubmit={onSubmit} />
    );

    fireEvent.change(
      getByLabelText('Email'),
      { target: { value: 'test@example.com' }}
    );

    fireEvent.change(
      getByLabelText('Password'),
      { target: { value: 'password123' }}
    );

    fireEvent.click(getByText('Submit'));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123'
      });
    });
  });
});

Tests de hooks React

// useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';

describe('useCounter', () => {
  test('should increment counter', () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  test('should decrement counter', () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(-1);
  });
});

// useFetch.test.js
import { renderHook } from '@testing-library/react-hooks';
import useFetch from './useFetch';

describe('useFetch', () => {
  beforeEach(() => {
    fetch.resetMocks();
  });

  test('should fetch data', async () => {
    const mockData = { id: 1, name: 'Test' };
    fetch.mockResponseOnce(JSON.stringify(mockData));

    const { result, waitForNextUpdate } = renderHook(() =>
      useFetch('api/data')
    );

    expect(result.current.loading).toBe(true);
    await waitForNextUpdate();

    expect(result.current.data).toEqual(mockData);
    expect(result.current.loading).toBe(false);
    expect(result.current.error).toBe(null);
  });

  test('should handle error', async () => {
    const error = new Error('API Error');
    fetch.mockRejectOnce(error);

    const { result, waitForNextUpdate } = renderHook(() =>
      useFetch('api/data')
    );

    expect(result.current.loading).toBe(true);
    await waitForNextUpdate();

    expect(result.current.data).toBe(null);
    expect(result.current.loading).toBe(false);
    expect(result.current.error).toEqual(error);
  });
});