본문으로 건너뛰기

Test 코드 작성 해보기

시작하기​

간단하게 test 코드를 작성하고 실행해 봅시다.

import {sum} from './sum';

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
yarn run test:watch
  • 결과

각 구문은 마치 책을 읽듯 직관적으로 읽힙니다. 1 + 2 가 3인지 테스트를 할것이다. sum(1,2) 의 결과 값이 3 이길 기대한다. 로 해석할 수 있을것 입니다. 조금 더 다양한 케이스를 다루는 테스트 코드를 살펴봅시다.

describe('add', () => {
it('should return 0 when given no arguments', () => {
expect(add()).toBe(0);
});

it('should return the sum of two numbers', () => {
expect(add(2, 3)).toBe(5);
});

it('should return the sum of multiple numbers', () => {
expect(add(1, 2, 3, 4, 5)).toBe(15);
});

it('should return the sum of numbers with decimal places', () => {
expect(add(0.1, 0.2)).toBe(0.3);
});
});
  • 결과

각각 구문은 어떤 정확히 어떤 역할을 하는걸까요?

describe(name, fn) ​

연관된 테스트를 Grupping 하는 함수 입니다.

test(name, fn) ​

alias: it(name, fn, timeout)

test, it 함수는 각 테스트 케이스를 지정하는 함수입니다.

expect(value)​

검증 하는 함수입니다. expect는 예시의 toBe 와 같은 matcher 로 다양한 검증을 진행 할 수 있습니다. 아래 링크에서 다양한 matcher 를 확인해 보세요.

@[Jest] matchers

가장 핵심 문법을 알아보았습니다. 이것이 절반에 해당한다고 생각합니다. 그럼 나머지 절반은 무엇일까요? 테스트 코드를 더 잘 작성하는 방법, 그리고 스스로 배워 나가야 할 다양한 방법과 스킬입니다. 다음 섹션에선 테스트 코드를 더 잘 작성하는 방법을 소개합니다.

Given/When/Then​

각각은 무엇을 의미할까요?

given​

테스트 코드를 하기위한 환경을 준비합니다. js 에선 데이터를 setting 하거나 intializing 하는 과정입니다.

when​

테스트하고자 하는 행위를 실행하는 과정입니다.

then​

실행된 결과를 검증 하는 과정입니다.

코드와 함께 확인해 보면서 각 로직이 테스트 코드에서 어떤 단계에 포함되는지 확인해 보세요.

type User = {
id: number;
name: string;
};

export class UserManager {
users: User[] = [];

addUser = (user: User) => {
this.users.push(user);
return this.users;
};

deleteUser = (id: number) => {
const updated = this.users.filter((user) => user.id !== id);
this.users = updated;
return this.users;
};
}

  • test code
import { UserManager } from '../manager';

// Given(준비)
const userManager = new UserManager();
const user = { id: 1, name: 'John Doe' };

describe('UserManager', () => {
test('addUser should add a user to the users array', () => {

// When(실행)
userManager.addUser(user);

// Then(검증)
expect(userManager.users).toHaveLength(1);
expect(userManager.users[0]).toEqual(user);
});

});

각 단계로 구분하여 명명하는 것 은, 당연한 행동을 거창하게 칭하는게 아닐까 싶습니다. 하지만 단계별로 로직을 구분함으로써, 조금 더 파악하기 쉬운 코드가 될 수 있습니다. 작은 코드에선 큰 의미가 없을 수 있지만, 테스트 로직이 복잡해진다면 위와 같이 단계를 구분하여 더 파악하기 쉬운 테스트 코드를 작성해 보세요.

테스트 원칙​

테스트 원칙은 많습니다! 유명한 First 원칙부터 7 원칙까지 다양한 원칙들을 다양한 사람들이 소개합니다. 이번 섹션에서는 가장 중요하다고 판단되는 2개만 살펴봅니다.

Isolated: 각각의 테스트는 독립적이어야 합니다.​

테스트를 더 작성해보며 독립적이지 않으면 어떤 문제가 발생하는지 확인해 봅시다.

import { UserManager } from '../manager';

const userManager = new UserManager();

describe('UserManager', () => {
test('addUser should add a user to the users array', () => {
const user = { id: 1, name: 'John Doe' };

userManager.addUser(user);

expect(userManager.users).toHaveLength(1);
expect(userManager.users[0]).toEqual(user);
});

test('deleteUser should remove a user from the users array', () => {
const user2 = { id: 2, name: 'John Steve' };
const user3 = { id: 3, name: 'Jane Smith' };

userManager.addUser(user2);
userManager.addUser(user3);
userManager.deleteUser(user2.id);

expect(userManager.users).toHaveLength(1);
// Error: Expected length: 1, Received length: 2
expect(userManager.users[0]).toEqual(user3);
// Error: Received Object: {"id": 1, "name": "John Doe"}
});
});

deleteUser의 테스트 코드에서 에러가 발생합니다.

애러 내용을 확인해 보니 테스트 코드가 놓친 부분은 그 전 addUser 테스트에서 추가 됐던 User 을 고려하지 못한것으로 파악이 됩니다. 그럼 위의 addUser 을 고려하여 테스트를 수정하면 해결이 될까요?

네 해결이 됩니다. 하지만 이는 잘못된 방법입니다. 우리는 한 기능을 테스트 하기위해 다른 테스트 코드를 파악해야하는 수고로움과 더불어, 실행 시점마다 달라 질 수 있는 테스트 결과에 신뢰를 잃게 됩니다.

Repeatable: 각 테스트는 실행 할 때 마다 결과가 동일해야 합니다.​

즉, 각 test 는 어떤 시점에서 실행이 되든 테스트 결과가 동일해야 합니다.

따라서 아래처럼 독립적으로 테스트를 진행하는것이 Isolate, Repeatlabe 위해 꼭 지켜야 할 원칙 중 하나입니다.

import { UserManager } from '../manager';

describe('UserManager', () => {
test('addUser should add a user to the users array', () => {
const userManager = new UserManager();
const user = { id: 1, name: 'John Doe' };

userManager.addUser(user);

expect(userManager.users).toHaveLength(1);
expect(userManager.users[0]).toEqual(user);
});

test('deleteUser should remove a user from the users array', () => {
const userManager = new UserManager();
const user2 = { id: 2, name: 'John Steve' };
const user3 = { id: 3, name: 'Jane Smith' };

userManager.addUser(user2);
userManager.addUser(user3);
userManager.deleteUser(user2.id);

expect(userManager.users).toHaveLength(1);
expect(userManager.users[0]).toEqual(user3);
});
});

Before Each​

위의 코드에서 given 에 해당하는 작업이 계속 반복됩니다. 한 줄이라 크게 불편하진 않지만 만약 5-6 줄이 반복된다면?setting 작업을 함수로 추상화해서 재사용 하는 방법도 있지만 jest 는 더욱 우아한 방법을 제공합니다.

beforeEach 함수는 각 test 가 실행되기 전 특정 로직을 실행 시킬 수 있습니다. 따라서 반복되는 given 의 단계를 생략 할 수 있습니다.


import { UserManager } from '../manager';

describe('UserManager', () => {
let userManager: UserManager;

beforeEach(() => {
userManager = new UserManager();
});

test('addUser should add a user to the users array', () => {
const user = { id: 1, name: 'John Doe' };
userManager.addUser(user);
expect(userManager.users).toHaveLength(1);
expect(userManager.users[0]).toEqual(user);
});

test('deleteUser should remove a user from the users array', () => {
const user1 = { id: 1, name: 'John Doe' };
const user2 = { id: 2, name: 'Jane Smith' };
userManager.addUser(user1);
userManager.addUser(user2);
userManager.deleteUser(user1.id);
expect(userManager.users).toHaveLength(1);
expect(userManager.users[0]).toEqual(user2);
});
});

beforeEach 말고도, @[Jest] beforeAll, @[Jest] afterAll, @[Jest] afterEach 등이 있으니 공식문서를 확인해 보세요.