Skip to main content

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 등이 있으니 공식문서를 확인해 보세요.