Unit Testing JavaScript dengan Jest: Panduan Lengkap


Testing adalah bagian penting dari software development yang sering diabaikan. Dengan unit testing, kamu bisa memastikan kode bekerja sesuai ekspektasi dan mencegah bug sebelum masuk ke production.

Mengapa Unit Testing?

  • Catch bugs early: Temukan bug sebelum deploy
  • Refactoring dengan percaya diri: Ubah kode tanpa takut merusak fitur
  • Documentation: Test sebagai dokumentasi behavior kode
  • Better design: Kode yang testable biasanya lebih modular

Setup Jest

Instalasi

# npm
npm install --save-dev jest

# yarn
yarn add --dev jest

Konfigurasi package.json

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Konfigurasi Jest (opsional)

Buat file jest.config.js:

module.exports = {
  testEnvironment: 'node',
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  collectCoverageFrom: ['src/**/*.js', '!src/**/*.test.js'],
};

Struktur Dasar

// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };
// math.test.js
const { add, subtract } = require('./math');

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

    test('should add negative numbers', () => {
      expect(add(-1, -2)).toBe(-3);
    });

    test('should add zero', () => {
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('subtract', () => {
    test('should subtract two numbers', () => {
      expect(subtract(5, 3)).toBe(2);
    });
  });
});

Menjalankan Test

npm test

Matchers

Jest menyediakan berbagai matchers untuk assertion:

Equality

test('equality matchers', () => {
  // Exact equality
  expect(2 + 2).toBe(4);

  // Object equality (deep)
  expect({ name: 'John' }).toEqual({ name: 'John' });

  // Not equal
  expect(2 + 2).not.toBe(5);
});

Truthiness

test('truthiness matchers', () => {
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();
  expect('hello').toBeDefined();
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
});

Numbers

test('number matchers', () => {
  expect(10).toBeGreaterThan(5);
  expect(10).toBeGreaterThanOrEqual(10);
  expect(5).toBeLessThan(10);
  expect(5).toBeLessThanOrEqual(5);

  // Floating point
  expect(0.1 + 0.2).toBeCloseTo(0.3);
});

Strings

test('string matchers', () => {
  expect('Hello World').toMatch(/World/);
  expect('Hello World').toContain('World');
  expect('Hello').toHaveLength(5);
});

Arrays

test('array matchers', () => {
  const fruits = ['apple', 'banana', 'orange'];

  expect(fruits).toContain('banana');
  expect(fruits).toHaveLength(3);
  expect(fruits).toEqual(expect.arrayContaining(['apple', 'orange']));
});

Objects

test('object matchers', () => {
  const user = {
    name: 'John',
    email: 'john@example.com',
    age: 25,
  };

  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty('name', 'John');
  expect(user).toMatchObject({ name: 'John', email: 'john@example.com' });
});

Exceptions

function throwError() {
  throw new Error('Something went wrong');
}

test('exception matchers', () => {
  expect(() => throwError()).toThrow();
  expect(() => throwError()).toThrow('Something went wrong');
  expect(() => throwError()).toThrow(Error);
});

Testing Async Code

Callbacks

function fetchData(callback) {
  setTimeout(() => {
    callback('data');
  }, 100);
}

test('callback test', (done) => {
  fetchData((data) => {
    expect(data).toBe('data');
    done();
  });
});

Promises

function fetchUser() {
  return Promise.resolve({ name: 'John' });
}

// Menggunakan return
test('promise test with return', () => {
  return fetchUser().then((user) => {
    expect(user.name).toBe('John');
  });
});

// Menggunakan resolves
test('promise test with resolves', () => {
  return expect(fetchUser()).resolves.toEqual({ name: 'John' });
});

Async/Await

async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

test('async/await test', async () => {
  const user = await getUser(1);
  expect(user.name).toBe('John');
});

test('async error handling', async () => {
  await expect(getUser(-1)).rejects.toThrow('User not found');
});

Mocking

Mock Functions

test('mock function', () => {
  const mockFn = jest.fn();

  mockFn('hello');
  mockFn('world');

  expect(mockFn).toHaveBeenCalled();
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith('hello');
});

Mock Return Values

test('mock return values', () => {
  const mockFn = jest.fn();

  mockFn.mockReturnValueOnce(10).mockReturnValueOnce(20).mockReturnValue(30);

  expect(mockFn()).toBe(10);
  expect(mockFn()).toBe(20);
  expect(mockFn()).toBe(30);
  expect(mockFn()).toBe(30);
});

Mock Implementations

test('mock implementation', () => {
  const mockFn = jest.fn((x) => x * 2);

  expect(mockFn(5)).toBe(10);
  expect(mockFn(3)).toBe(6);
});

Mocking Modules

// userService.js
const axios = require('axios');

async function getUser(id) {
  const response = await axios.get(`/api/users/${id}`);
  return response.data;
}

module.exports = { getUser };
// userService.test.js
const axios = require('axios');
const { getUser } = require('./userService');

jest.mock('axios');

test('should fetch user', async () => {
  const mockUser = { id: 1, name: 'John' };
  axios.get.mockResolvedValue({ data: mockUser });

  const user = await getUser(1);

  expect(user).toEqual(mockUser);
  expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});

Spying

const calculator = {
  add: (a, b) => a + b,
  multiply: (a, b) => a * b,
};

test('spy on method', () => {
  const spy = jest.spyOn(calculator, 'add');

  calculator.add(2, 3);

  expect(spy).toHaveBeenCalledWith(2, 3);

  spy.mockRestore();
});

Setup dan Teardown

describe('Database tests', () => {
  // Jalankan sekali sebelum semua test
  beforeAll(async () => {
    await db.connect();
  });

  // Jalankan sekali setelah semua test
  afterAll(async () => {
    await db.disconnect();
  });

  // Jalankan sebelum setiap test
  beforeEach(async () => {
    await db.clear();
  });

  // Jalankan setelah setiap test
  afterEach(() => {
    jest.clearAllMocks();
  });

  test('should create user', async () => {
    // test code
  });
});

Testing Best Practices

1. Arrange-Act-Assert (AAA)

test('should calculate total price', () => {
  // Arrange
  const items = [
    { price: 100, quantity: 2 },
    { price: 50, quantity: 3 },
  ];

  // Act
  const total = calculateTotal(items);

  // Assert
  expect(total).toBe(350);
});

2. Test One Thing at a Time

// ❌ Bad - testing multiple things
test('user operations', () => {
  const user = createUser('John');
  expect(user.name).toBe('John');

  updateUser(user, { age: 25 });
  expect(user.age).toBe(25);

  deleteUser(user);
  expect(user.deleted).toBe(true);
});

// ✅ Good - separate tests
test('should create user with name', () => {
  const user = createUser('John');
  expect(user.name).toBe('John');
});

test('should update user age', () => {
  const user = createUser('John');
  updateUser(user, { age: 25 });
  expect(user.age).toBe(25);
});

3. Use Descriptive Test Names

// ❌ Bad
test('test1', () => {});

// ✅ Good
test('should return empty array when no items match filter', () => {});

4. Don’t Test Implementation Details

// ❌ Bad - testing internal state
test('should set internal flag', () => {
  const cart = new Cart();
  cart.addItem({ id: 1 });
  expect(cart._items.length).toBe(1); // internal
});

// ✅ Good - testing behavior
test('should have one item after adding', () => {
  const cart = new Cart();
  cart.addItem({ id: 1 });
  expect(cart.getItemCount()).toBe(1);
});

Code Coverage

Jalankan test dengan coverage:

npm test -- --coverage

Output akan menunjukkan:

  • Statements: Persentase statement yang dieksekusi
  • Branches: Persentase branch (if/else) yang dieksekusi
  • Functions: Persentase function yang dipanggil
  • Lines: Persentase baris yang dieksekusi

Kesimpulan

Unit testing dengan Jest membantu kamu:

  • Menulis kode yang lebih reliable
  • Refactor dengan percaya diri
  • Dokumentasi behavior kode
  • Catch bugs lebih awal

Mulai dengan test sederhana, lalu tingkatkan coverage secara bertahap. Ingat, test yang baik adalah test yang mudah dibaca dan di-maintain.


Referensi:

Komentar

Real-time

Memuat komentar...

Tulis Komentar

Email tidak akan ditampilkan

0/2000 karakter

Catatan: Komentar akan dimoderasi sebelum ditampilkan. Mohon bersikap sopan dan konstruktif.