Skip to main content
โšก Calmops

Unit Testing: Jest, Mocha, Vitest

Unit Testing: Jest, Mocha, Vitest

Unit testing is fundamental for code quality. This article covers popular testing frameworks and best practices.

Introduction

Unit testing provides:

  • Code quality assurance
  • Regression prevention
  • Documentation
  • Refactoring confidence
  • Bug detection

Understanding unit testing helps you:

  • Write testable code
  • Catch bugs early
  • Maintain code quality
  • Document behavior
  • Refactor safely

Jest Testing Framework

Jest Setup

# โœ… Good: Install Jest
npm install --save-dev jest

# โœ… Good: Configure Jest
# package.json
{
  "jest": {
    "testEnvironment": "node",
    "collectCoverage": true,
    "coverageDirectory": "coverage",
    "testMatch": ["**/__tests__/**/*.js", "**/?(*.)+(spec|test).js"]
  }
}

# โœ… Good: Run tests
npm test
npm test -- --watch
npm test -- --coverage

Jest Tests

// โœ… Good: Basic test
describe('Math', () => {
  test('adds numbers correctly', () => {
    expect(2 + 2).toBe(4);
  });

  test('subtracts numbers correctly', () => {
    expect(5 - 3).toBe(2);
  });
});

// โœ… Good: Test with setup and teardown
describe('Database', () => {
  let db;

  beforeAll(async () => {
    db = await connectDB();
  });

  afterAll(async () => {
    await db.close();
  });

  beforeEach(async () => {
    await db.clear();
  });

  test('saves user', async () => {
    const user = await db.saveUser({ name: 'John' });
    expect(user.name).toBe('John');
  });
});

// โœ… Good: Mocking
jest.mock('../services/userService');

describe('UserController', () => {
  test('gets user', async () => {
    userService.getUser.mockResolvedValue({ id: 1, name: 'John' });
    
    const user = await userController.getUser(1);
    
    expect(user.name).toBe('John');
    expect(userService.getUser).toHaveBeenCalledWith(1);
  });
});

// โœ… Good: Spying
describe('Logger', () => {
  test('logs message', () => {
    const spy = jest.spyOn(console, 'log');
    
    logger.log('test');
    
    expect(spy).toHaveBeenCalledWith('test');
    spy.mockRestore();
  });
});

Mocha Testing Framework

Mocha Setup

# โœ… Good: Install Mocha
npm install --save-dev mocha chai

# โœ… Good: Configure Mocha
# .mocharc.json
{
  "require": "test/setup.js",
  "spec": "test/**/*.test.js",
  "timeout": 5000
}

# โœ… Good: Run tests
npm test
npm test -- --watch
npm test -- --reporter json

Mocha Tests

const { expect } = require('chai');

// โœ… Good: Basic test
describe('Math', () => {
  it('adds numbers correctly', () => {
    expect(2 + 2).to.equal(4);
  });

  it('subtracts numbers correctly', () => {
    expect(5 - 3).to.equal(2);
  });
});

// โœ… Good: Async tests
describe('Database', () => {
  let db;

  before(async () => {
    db = await connectDB();
  });

  after(async () => {
    await db.close();
  });

  beforeEach(async () => {
    await db.clear();
  });

  it('saves user', async () => {
    const user = await db.saveUser({ name: 'John' });
    expect(user.name).to.equal('John');
  });
});

// โœ… Good: Hooks
describe('API', () => {
  let server;

  before(() => {
    server = startServer();
  });

  after(() => {
    server.close();
  });

  it('returns users', async () => {
    const response = await fetch('http://localhost:3000/api/users');
    expect(response.status).to.equal(200);
  });
});

Vitest Testing Framework

Vitest Setup

# โœ… Good: Install Vitest
npm install --save-dev vitest

# โœ… Good: Configure Vitest
# vitest.config.js
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html']
    }
  }
});

# โœ… Good: Run tests
npm test
npm test -- --watch
npm test -- --coverage

Vitest Tests

import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';

// โœ… Good: Basic test
describe('Math', () => {
  it('adds numbers correctly', () => {
    expect(2 + 2).toBe(4);
  });

  it('subtracts numbers correctly', () => {
    expect(5 - 3).toBe(2);
  });
});

// โœ… Good: Mocking with Vitest
vi.mock('../services/userService');

describe('UserController', () => {
  it('gets user', async () => {
    const { userService } = await import('../services/userService');
    userService.getUser.mockResolvedValue({ id: 1, name: 'John' });
    
    const user = await userController.getUser(1);
    
    expect(user.name).toBe('John');
    expect(userService.getUser).toHaveBeenCalledWith(1);
  });
});

// โœ… Good: Snapshot testing
describe('Component', () => {
  it('renders correctly', () => {
    const component = render(<Button>Click me</Button>);
    expect(component).toMatchSnapshot();
  });
});

Testing Best Practices

Test Structure

// โœ… Good: AAA pattern (Arrange, Act, Assert)
describe('UserService', () => {
  it('creates user with valid data', () => {
    // Arrange
    const userData = { name: 'John', email: '[email protected]' };
    
    // Act
    const user = userService.createUser(userData);
    
    // Assert
    expect(user.name).toBe('John');
    expect(user.email).toBe('[email protected]');
  });
});

// โœ… Good: Descriptive test names
describe('UserService', () => {
  it('should create user with valid data', () => {
    // Test
  });

  it('should throw error with invalid email', () => {
    // Test
  });

  it('should update user name', () => {
    // Test
  });
});

// โœ… Good: Test one thing per test
describe('UserService', () => {
  it('creates user', () => {
    const user = userService.createUser({ name: 'John' });
    expect(user.name).toBe('John');
  });

  it('saves user to database', () => {
    const user = userService.createUser({ name: 'John' });
    expect(db.users).toContain(user);
  });
});

Mocking and Stubbing

// โœ… Good: Mock external dependencies
jest.mock('axios');

describe('UserAPI', () => {
  it('fetches users', async () => {
    axios.get.mockResolvedValue({
      data: [{ id: 1, name: 'John' }]
    });

    const users = await userAPI.getUsers();

    expect(users).toHaveLength(1);
    expect(axios.get).toHaveBeenCalledWith('/api/users');
  });
});

// โœ… Good: Stub functions
describe('Logger', () => {
  it('logs message', () => {
    const logSpy = jest.fn();
    const logger = new Logger(logSpy);

    logger.log('test');

    expect(logSpy).toHaveBeenCalledWith('test');
  });
});

Test Coverage

Coverage Configuration

// โœ… Good: Coverage thresholds
// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/index.js',
    '!src/**/*.test.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

// โœ… Good: Generate coverage report
npm test -- --coverage

// โœ… Good: View coverage report
open coverage/lcov-report/index.html

Best Practices

  1. Write testable code:

    // โœ… Good: Testable code
    function calculateTotal(items) {
      return items.reduce((sum, item) => sum + item.price, 0);
    }
    
    // โŒ Bad: Hard to test
    function calculateTotal() {
      const items = getItemsFromDatabase();
      return items.reduce((sum, item) => sum + item.price, 0);
    }
    
  2. Test behavior, not implementation:

    // โœ… Good: Test behavior
    expect(userService.createUser(data)).toHaveProperty('id');
    
    // โŒ Bad: Test implementation
    expect(userService.createUser).toHaveBeenCalled();
    
  3. Keep tests simple:

    // โœ… Good: Simple test
    it('adds numbers', () => {
      expect(add(2, 3)).toBe(5);
    });
    
    // โŒ Bad: Complex test
    it('does everything', () => {
      // Multiple assertions
      // Multiple setups
      // Hard to understand
    });
    

Summary

Unit testing is essential. Key takeaways:

  • Choose appropriate framework
  • Write testable code
  • Follow AAA pattern
  • Mock external dependencies
  • Test behavior
  • Maintain coverage
  • Keep tests simple
  • Test one thing per test

Next Steps

Comments