[테스트] How To Test #1. Unit Test (feat. jest)
Web Frontend Developer

[테스트] How To Test #1. Unit Test (feat. jest)

이번 포스팅에서는 테스트에 대한 이야기를 해보려고 한다. 첫 번째 주제는 단위 테스트이다.

 

Jest 설치하기

유닛 테스트를 할 수 있는 라이브러리는 여러 가지가 있지만, 나는 jest가 지금 시점에서는 가장 좋은 옵션이라고 생각을 한다. 자바스크립트를 사용하는 프론트엔드, 백엔드 모두 활발하게 쓰이고 있으며, test runner와 test matcher, test mock 등을 별도의 여러 개의 라이브러리를 설치해 주지 않고 이 jest 프레임워크 안에서 한꺼번에 할 수 있기 때문이다.

그러면 jest 를 프로젝트에 설치하는 과정부터 하나씩 살펴보도록 하자. 먼저 jest 라이브러리를 설치한다.

yarn add --dev jest

설치 후에는 package.json 파일에 다음과 같이 스크립트를 추가해 준다.

{
  "scripts": {
    "test": "jest"
  }
}

테스트 파일은 일반적으로 __test__라는 디렉토리 안에서 *.test.js 또는 *.spec.js 파일으로 만드는데 예를 들어 두 숫자를 더하는 sum이라는 함수를 테스트 하고 싶다면 다음과 같이 sum.test.js 파일을 작성한 후에 yarn test를 하면 된다.

const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});
> yarn test

PASS  ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

yarn test를 수행하면 jest는 기본적으로 프로젝트 내의 모든 테스트 파일들을 찾아서 실행한다. 파일의 postfix가 test.js, spec.js 이거나 __test__ 디렉토리 안에 있는 파일들은 모두 테스트 파일으로 인식한다.

 

Matcher

Jest에서는 거의 우리가 예상할 수 있는 모든 경우에 대한 Matcher 함수를 제공한다. expect는 다양한 matcher를 통해 어떤 값을 검증할 수 있게 해준다. 원시 타입(prmitive type)의 matcher로서는 위에서 사용한 toBe()가 사용된다.

test('the best flavor is grapefruit', () => {
  expect(bestLaCroixFlavor()).toBe('grapefruit');
});

 

toEqual()

하지만 원시 타입이 아닌 객체의 경우는 toBe()를 사용할 수 없으며 toEqual()을 사용해야 한다. toEqual()은 객체의 모든 프로퍼티들을 재귀적으로 비교하여 주기 때문이다.

const can1 = {
  flavor: 'grapefruit',
  ounces: 12,
};
const can2 = {
  flavor: 'grapefruit',
  ounces: 12,
};

describe('the La Croix cans on my desk', () => {
  test('have all the same properties', () => {
    expect(can1).toEqual(can2);
  });
  test('are not the exact same can', () => {
    expect(can1).not.toBe(can2);
  });
});

 

toBeTruthy(), toBeFalsy()

toEqual() 다음으로 정말 많이 쓰이는 matcher는 아마 toBeTruthy()toBeFalsy()일 것이다. 느슨한 타입 언어인 자바스크립트는 자바 같은 강한 타입 기반 언어처럼 true와 false가 boolean 타입에 한정되지 않는다. 숫자 1, 문자열 'abc' 등의 값들은 true로 간주가 되고, 숫자 0, null, undefined 같은 값들은 false로 간주된다. 이러한 규칙에 따라서 toBeTruthy()toBeFalsy()가 동작한다.

drinkSomeLaCroix();
if (thirstInfo()) {
  drinkMoreLaCroix();
}

test('drinking La Croix leads to having thirst info', () => {
  drinkSomeLaCroix();
  expect(thirstInfo()).toBeTruthy();
});
drinkSomeLaCroix();
if (!getErrors()) {
  drinkMoreLaCroix();
}

test('drinking La Croix does not lead to errors', () => {
  drinkSomeLaCroix();
  expect(getErrors()).toBeFalsy();
});

 

toHaveLength(), toContain()

배열의 경우 배열 길이를 체크할 때 toHaveLength()를 사용하고, 특정 원소 존재 여부를 테스트 하는 경우에 toContain()을 사용한다.

test("array", () => {
  const colors = ["Red", "Yellow", "Blue"];
  expect(colors).toHaveLength(3);
  expect(colors).toContain("Yellow");
  expect(colors).not.toContain("Green");
});

 

toMatch()

문자열의 경우 단순히 toBe()를 사용해서 문자열이 정확히 일치하는지 여부를 체크할 수도 있지만, 정규식 기반의 테스트가 필요할 수도 있는데, 이러한 경우 toMatch()를 사용할 수 있다.

describe('an essay on the best flavor', () => {
  test('mentions grapefruit', () => {
    expect(essayOnTheBestFlavor()).toMatch(/grapefruit/);
    expect(essayOnTheBestFlavor()).toMatch(new RegExp('grapefruit'));
  });
});

 

toThrow()

예외 발생 여부를 테스트 해야 할 때는 toThrow() 함수를 사용할 수 있다. toThrow() 함수는 인자도 받는데, 문자열을 넘기면 예외 메시지를 비교하고 정규식을 넘기면 정규식 체크도 할 수 있다. toThrow() 함수를 사용할 때 하기 쉬운 실수 중 하나가 expect() 함수에 넘기는 검증 대상을 함수로 감싸줘야 하는 부분을 빼먹는 것인데 빼먹지 않도록 한다.

test('throws on octopus', () => {
  expect(() => {
    drinkFlavor('octopus');
  }).toThrow();
});

// Wrong
test('throws on octopus', () => {
  expect(drinkFlavor('octopus')).toThrow();
});

matcher는 이 외에도 정말 많이 있지만, 너무 글의 분량이 길어질 것 같아서 자주 쓰이는 함수들만 정리했다. 더 많은 matcher들을 알고 싶다면 jest 공식 문서를 참고하길 바란다.

 

비동기 처리

자바스크립트에서 비동기 코드를 짤 일은 많고, 따라서 당연히 비동기 테스트를 해야 할 일도 많이 있다. Jest Runner가 비동기 코드에 대한 테스트라는 사실을 인지할 수 있도록 테스트를 작성해 주지 않으면 예상치 못한 테스트 결과에 당황할 수 있다.

 

콜백 함수 테스트

아래와 같이 fetchUser() 함수는 0.1초 후에 인자로 받은 id 값을 가진 사용자 객체를 콜백 함수의 인자로 보내는 함수이다. 실제 코드라면 DB를 조회하는 로직 등이 포함이 되어야 하겠지만, 예제이므로 setTimeout을 사용하여 지연을 해주었다.

function fetchUser(id, cb) {
  setTimeout(() => {
    console.log("wait 0.1 sec.");
    const user = {
      id: id,
      name: "User" + id,
      email: id + "@test.com",
    };
    cb(user);
  }, 100);
}

이 콜백 함수가 유저 정보를 올바르게 받아오는지를 테스트 해보자. 먼저 위에서 배웠던 toEqual()을 사용해 볼 수 있다.

test("fetch a user", () => {
  fetchUser(1, (user) => {
    expect(user).toEqual({
      id: 1,
      name: "User1",
      email: "1@test.com",
    });
  });
});
> yarn test

> my-jest@1.0.0 test /my-jest
> jest

 PASS  ./async.test.js
  ✓ fetch a user (1ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.756s, estimated 1s
Ran all test suites.

이 방식으로 테스트를 해보면 성공은 하지만 뭔가 이상하다. 우리가 분명히 setTimeout으로 시간을 지연시켜 주었는데, 그만큼 지연이 일어나지 않기 때문이다. 자세히 살펴보면 위와 같은 코드는 Jest Runner가 테스트 함수를 최대한 빨리 호출해 주기 때문에 콜백 함수가 호출될 기회를 얻지 못했고, 콜백 함수 내의 toEqual() matcher 함수도 호출이 되지 못한 것이다.

이 문제를 해결하기 위해서는 Jest Runner에게 이 테스트 함수가 비동기 코드를 실행하니 콜백 함수가 호출되는지를 확인해 달라고 요청하는 것이다. 아래와 같이 실행할 함수가 done 이라는 함수 인자를 받도록 수정하고, 이 done 함수를 콜백 함수의 가장 마지막에 호출하도록 수정한다.

test("fetch a user", (done) => {
  fetchUser(2, (user) => {
    expect(user).toEqual({
      id: 1,
      name: "User1",
      email: "1@test.com",
    });
    done();
  });
});
> yarn test

> my-jest@1.0.0 test /my-jest
> jest

 FAIL  ./async.test.js
  ✕ fetch a user (124ms)

  ● fetch a user

    expect(received).toEqual(expected)

    Difference:

    - Expected
    + Received

      Object {
    -   "email": "1@test.com",
    -   "id": 1,
    -   "name": "User1",
    +   "email": "2@test.com",
    +   "id": 2,
    +   "name": "User2",
      }

      13 | test('fetch a user', done => {
      14 |   fetchUser(2, user => {
    > 15 |     expect(user).toEqual({
         |                  ^
      16 |       id: 1,
      17 |       name: 'User1',
      18 |       email: '1@test.com'

      at toEqual (async.test.js:15:18)
      at cb (async.test.js:9:5)

  console.log async.test.js:3
    wait 0.1 sec.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.894s, estimated 1s
Ran all test suites.

수정된 테스트 코드는 id 값을 2로 인자로 보내서 함수를 실행했는데 id가 1인 유저 정보를 받아서 우리가 의도한 대로 올바르게 실패하는 코드가 돌아갔고 수행 시간도 124ms로 우리가 의도한 0.1s의 지연이 적용이 된 것을 알 수 있다.

 

Promise 테스트

이번에는 Promise를 사용해서 구현된 비동기 코드에 대한 테스트를 어떻게 작성하는지 살펴보자.
위의 fetchUserPromise를 이용해서 다음과 같이 재작성 될 수 있다.

function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("wait 0.1 sec.");
      const user = {
        id: id,
        name: "User" + id,
        email: id + "@test.com",
      };
      resolve(user);
    }, 100);
  });
}

먼저 실패하는 테스트를 작성 해 보고 실제로 실패하는지 확인을 해 본다.

test("fetch a user", () => {
  fetchUser(2).then((user) => {
    expect(user).toEqual({
      id: 1,
      name: "User1",
      email: "1@test.com",
    });
  });
});
> yarn test

> my-jest@1.0.0 test /my-jest
> jest

 PASS  ./promise.test.js
  ✓ fetch a user (1ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.766s, estimated 1s
Ran all test suites.

이상하게 실패하지 않았다(?) 이 부분 역시 setTimeout 100ms를 걸어 주었음에도 수행 시간이 1ms 였다는 것은 fetchUser() 함수가 리턴한 Promise 객체의 then() 메서드가 실행될 기회도 얻지 못했던 것으로 추정된다.

이 문제를 해결하기 위해서는 테스트 함수에 return을 추가해 주면 된다. 테스트 함수가 Promise를 리턴하면 Jest Runner는 리턴된 Promiseresolve 될 때 까지 기다려준다.

test("fetch a user", () => {
  return fetchUser(2).then((user) => {
    expect(user).toEqual({
      id: 1,
      name: "User1",
      email: "1@test.com",
    });
  });
});
> yarn test

> my-jest@1.0.0 test /my-jest
> jest

 FAIL  ./promise.test.js
  ✕ fetch a user (117ms)

  ● fetch a user

    expect(received).toEqual(expected)

    Difference:

    - Expected
    + Received

      Object {
    -   "email": "1@test.com",
    -   "id": 1,
    -   "name": "User1",
    +   "email": "2@test.com",
    +   "id": 2,
    +   "name": "User2",
      }

      15 | test('fetch a user', () => {
      16 |   return fetchUser(2).then(user => {
    > 17 |     expect(user).toEqual({
         |                  ^
      18 |       id: 1,
      19 |       name: 'User1',
      20 |       email: '1@test.com'

      at toEqual (promise.test.js:17:18)

  console.log promise.test.js:4
    wait 0.1 sec.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.021s
Ran all test suites.

 

Async/Await 테스트

마지막으로 async/await 키워드를 살펴보자. 지금까지 언급했던 세 가지 방법 중에 가장 동기 코드처럼 보이게 테스트를 작성할 수 있다.

test("fetch a user", async () => {
  const user = await fetchUser(2);
  expect(user).toEqual({
    id: 1,
    name: "User1",
    email: "1@test.com",
  });
});
> npm test

> my-jest@1.0.0 test /my-jest
> jest

 FAIL  ./promise.test.js
  ✕ fetch a user (114ms)

  ● fetch a user

    expect(received).toEqual(expected)

    Difference:

    - Expected
    + Received

      Object {
    -   "email": "1@test.com",
    -   "id": 1,
    -   "name": "User1",
    +   "email": "2@test.com",
    +   "id": 2,
    +   "name": "User2",
      }

      15 | test('fetch a user', async () => {
      16 |   const user = await fetchUser(2);
    > 17 |   expect(user).toEqual({
         |                ^
      18 |     id: 1,
      19 |     name: 'User1',
      20 |     email: '1@test.com'

      at Object.toEqual (promise.test.js:17:16)

  console.log promise.test.js:4
    wait 0.1 sec.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.997s, estimated 1s
Ran all test suites.

테스트가 올바르게 실패했음을 알 수 있다.

 

Mocking

jest 테스트 프레임워크를 사용할 때 가진 장점 중 하나는 다른 라이브러리 설치가 필요가 없다는 것이다. mocking은 단위 테스트를 작성 시, 해당 코드가 의존하는 부분을 가짜(mock)로 대체하는 기법을 말한다. 일반적으로 테스트하려는 코드가 의존하는 부분을 직접 생성하기가 어려운 경우 또는 부담스러운 경우에 mocking을 많이 사용한다.

예를 들어, DB에서 데이터를 삭제하는 코드에 대한 단위 테스트를 작성한다고 했을 때, 이 과정에서 실제 DB를 사용하면 어떻게 될까?

  • DB 접속과 같이 네트워크나 I/O 작업이 포함된 테스트는 실행 속도가 떨어진다.
  • 이러한 테스트 케이스가 많아진다면 작은 속도 저하 이슈가 쌓여서 심각한 상황을 초래할 수 있고, CI/CD에서 테스팅을 자동화 시켜 놓았다면 더 큰 문제가 발생할 수 있다.
  • 로직을 테스트 하는 코드보다, DB에 연결하고 트랜잭션을 생성하고 쿼리를 전송하는 코드가 더 길어질 수 있다. 배 보다 배꼽이 더 커지는 격이다.
  • 테스트 실행 순간에 만약 DB가 내려가 있다면 해당 테스트는 실패한다. 이는 테스트가 인프라 환경에 의존적임을 의미한다.
  • 테스트 후 DB에서 바뀐 데이터를 원복하거나 트랜잭션을 롤백(rollback) 해줘야 하는데 이 작업 역시 번거롭다.

여러가지 예상 가능한 문제 점들을 나열했는데, 무엇보다 단위 테스트는 특정 기능만 분리해서 그 기능이 잘 동작하는지를 테스트 해야 하는데 그 정신(?)에 부합하지 않는 작업들을 하게 될 수 밖에 없다.

mocking은 이러한 상황에서 실제 객체인 것처럼 존재하는 가짜 객체를 생성하는 메커니즘을 제공한다. 테스트가 실행 되는 동안 가짜 객체에 어떤 일들이 발생했는지를 기억하기 때문에 가짜 객체가 내부적으로 어떻게 사용되는지 검증할 수 있다.

결론적으로 mocking을 사용하면 실제 객체를 사용하는 것보다 훨씬 가볍고 빠르게 실행이 되면서 항상 동일한 결과를 내는 단위 테스트를 작성할 수 있게 된다.

 

jest.fn()

jest는 가짜 함수(mock function)를 생성할 수 있도록 jest.fn() 함수를 제공한다. 이 가짜 함수는 일반 자바스크립트 함수와 동일한 방식으로 인자를 넘겨서 호출할 수 있다.

const mockFn = jest.fn();

mockFn();
mockFn(1);
mockFn("a");
mockFn([1, 2], { a: "b" });

가짜 함수의 호출 결과는 모두 undefined이다. 아직 어떤 값을 리턴해야 할지 알려주지 않았기 때문이다. mockReturnValue(value) 함수를 이용해서 가짜 함수가 어떤 값을 리턴해야 할지 설정해 줄 수 있다.

mockFn.mockReturnValue("I am a mock!");
console.log(mockFn()); // I am a mock!

뿐만 아니라 mockImplementation(code) 함수를 이용하면 아예 해당 함수를 통째로 재구현 해 버릴 수도 있다.

mockFn.mockImplementation((name) => `I am ${name}!`);
console.log(mockFn("Owen")); // I am Owen!

테스트를 작성할 때 가짜 함수가 유용한 이유는 가짜 함수는 자신이 어떻게 호출되었는지를 모두 기억한다는 점이다.

mockFn("a");
mockFn(["b", "c"]);

expect(mockFn).toBeCalledTimes(2);
expect(mockFn).toBeCalledWith("a");
expect(mockFn).toBeCalledWith(["b", "c"]);

위와 같이 가짜 함수 용으로 설계된 jest matcher인 toBeCalled 함수를 사용하면 가짜 함수가 몇 번 호출되었고 인자로 무엇이 넘어왔는지를 검증할 수 있다.

 

jest.spyOn()

mocking에는 스파이(spy)라는 개념이 있다. 우리가 현실에서 보는 스파이는 어떤 기관이나 조직에 들어가서 '몰래' 정보를 캐내는 역할을 한다. 테스트 코드를 작성할 때도, 어떤 객체에 속한 함수의 구현을 가짜로 대체하지 않고 해당 함수의 호출 여부와 어떻게 호출되었는지만을 알아내야 할 때가 있다.

이럴 때 jest.spyOn(obj, method) 함수를 사용하면 된다.

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

const spyFn = jest.spyOn(calculator, "add");

const result = calculator.add(2, 3);

expect(spyFn).toBeCalledTimes(1);
expect(spyFn).toBeCalledWith(2, 3);
expect(result).toBe(5);

위의 예제를 보면 jest.spyOn() 함수를 통해 calculator 객체의 add 함수에 스파이를 붙였음을 알 수 있다. 이를 통해 add 함수를 호출한 후에 호출 횟수와 어떤 인자가 넘어갔는지를 검증할 수 있다. 이는 위에서 가짜 함수로 대체한 것과는 다르며 결과 값은 원래 구현대로 2+3=5가 나타나는 것을 알 수 있다.

 

테스트 작성하기

방금 우리가 살펴본 jest.fn()jest.spyOn()을 사용해서 어떻게 테스트를 작성할 수 있을지 살펴보자.

아래 예제는 axios를 가지고 REST API를 호출해서 데이터를 조회하는 함수가 있는 모듈이다. 이 모듈에서 id 값을 가지고 사용자 정보를 받아오는 findOne() 함수에 대한 테스트를 작성해 보도록 하자.

const axios = require("axios");
const API_ENDPOINT = "https://jsonplaceholder.typicode.com";

module.exports = {
  findOne(id) {
    return axios
      .get(`${API_ENDPOINT}/users/${id}`)
      .then((response) => response.data);
  },
};

여기서 findOne() 함수가 외부 API 연동을 통해 사용자 정보를 조회하는지 테스트를 하려면 어떻게 해야 할까?

이 함수는 내부적으로 axios 객체의 get 함수를 사용하기 때문에, 여기에 스파이를 붙이면 쉽게 알아낼 수 있다.

const axios = require("axios");
const userService = require("./userService");

test("findOne fetches data from the API endpoint", async () => {
  const spyGet = jest.spyOn(axios, "get");
  await userService.findOne(1);
  expect(spyGet).toBeCalledTimes(1);
  expect(spyGet).toBeCalledWith(`https://jsonplaceholder.typicode.com/users/1`);
});

이 테스트도 훌륭하지만 문제가 하나 있는데, 만약 API 서버가 죽었거나, 네트워크가 끊어졌다면 테스트가 실패한다. 따라서 이는 테스트가 deterministic 해야 한다(언제 실행되든 항상 같은 결과를 내야 한다)는 원칙에 위배가 된다. 왜냐하면 단위테스트가 단독으로 고립된 것이 아닌, 외부에 의존적이기 때문이다.

이를 해결하기 위해서는 axios 객체의 get 함수가 항상 안정적으로 결과를 반환하도록 mocking해야 한다. 아래처럼 axios.get이 어떤 고정된 결과를 반환할 수 있도록 가짜 함수로 대체를 해 주면 된다.

const axios = require("axios");
const userService = require("./userService");

test("findOne returns what axios get returns", async () => {
  axios.get = jest.fn().mockResolvedValue({
    data: {
      id: 1,
      name: "Wonjong Oh",
    },
  });

  const user = await userService.findOne(1);
  expect(user).toHaveProperty("id", 1);
  expect(user).toHaveProperty("name", "Wonjong Oh");
});

이렇게 테스트하는 입장에서 통제할 수 없는 부분을 mocking을 통해 외부 환경에 의존하지 않고 얼마든지 독립적으로 테스트를 작성할 수 있다.

 

그래서 무엇을 우리는 테스트를 해야 하는가?

간단하게 단위 테스트 프레임워크인 jest를 가지고 어떤 식으로 단위 테스트를 수행할 수 있는지 살펴보았다. 마지막으로 그렇다면 우리가 어떤 것을 테스트해야 하는지에 대해 이야기 해보고 해당 포스팅을 마무리 하려고 한다.

우선 UI 테스팅, E2E 테스팅은 이후 포스팅에서 다룰 계획이므로 이번 포스팅에서는 논의하지 않는다.

 

동작을 통한 부수 효과를 기대하는 비즈니스 로직

먼저 가장 중요한 건 우리가(우리 팀이, 기획자가, 개발자가 등등 프로덕트를 만드는 주체가) 의도한 대로 동작하는 것이 가장 중요하다. 예를 들어 이커머스 도메인이라면 상품 상세 페이지에서 장바구니 버튼을 눌렀을 때 해당 상품이 장바구니에 담기는 것을 의도했으므로, 프로그램이 계속해서 업데이트가 되더라도 해당 장바구니 담기 동작은 항상 정상적으로 동작해야 하며, 그렇지 않은 경우를 우리는 버그(bug)라고 한다.

그렇다면 우리는 여기서 사용자의 동작에 주목을 하고 이 동작으로 인해 어떤 부수 효과(side effect)가 발생할 것인지에 대해서 테스트를 해 주어야 한다. 위의 장바구니 예제의 경우 장바구니 버튼을 클릭(onClick) 하는 이벤트가 발생하면 물건이 장바구니에 담기는 부수 효과가 일어나는 식으로 말이다. 이러한 작은 동작 -> 부수 효과 로직을 프론트엔드에서는 비즈니스 로직(business logic)이라고 한다. 개발자가 의도한 동작에서 어떤 부수 효과가 일어나는지를 테스트 하는 것이 중요하다. 이 외의 것들은 테스트 하지 않는다. 예를 들면 버튼이 제대로 그려지는가? 버튼이 제대로 눌리는가?(클릭 이벤트 자체가 정상적으로 실행되는가) 등 말이다.

테스트를 할 때는 여러가지 방법이 있으며 테스트 주도 개발(TDD, Test Driven Development) 같은 개발 방법론도 있다. 이번 포스팅에서는 그러한 주제에 대해서는 자세하게 다루지는 않으려고 한다. 필자도 해당 분야에서는 전문가가 아니기 때문이다. 다만 필자가 지금까지 주로 사용했던 방법들을 중심으로 이야기를 해 보려고 한다.

 

given-when-then 패턴

필자는 해당 패턴이 가장 익숙해서 들고 와 보았다. 이 방법이 정답이 절대 아니며 무수히 많은 테스트 방법들이 있다. 더 좋은 방법은 댓글로 남겨주면 고마울 것 같다.

테스트를 작성할 때 우리가 살펴야 하는 것들은 다음과 같은 것들이 있을 것이다.

  • 테스트 케이스의 목적
  • 최종 확인해야 하는 예상 결과 값
  • 어떠한 flow를 검증할 것인지
  • 이 flow를 검증하기 위해 어떤 가정이 필요한지

예를 들어 앞에서 살펴본 장바구니 담기 비즈니스 로직을 테스트 한다고 하면 다음과 같이 작성을 해볼 수 있을 것이다.

  • 테스트 케이스의 목적 : 장바구니 버튼 클릭 시에 해당 상품이 해당 유저의 장바구니에 담기는지를 확인한다.
  • 최종 확인해야 하는 예상 결과 값 : 해당 유저의 장바구니 리스트에 해당 상품이 포함된다. (여기에 추가적으로 기존에 있다면 중복해서 포함되지 않는다가 포함될 수도 있겠다.)
  • 어떠한 flow를 검증할 것인지 : 상세 페이지에서 장바구니 버튼을 클릭 -> 해당 유저의 장바구니 페이지에서 해당 상품을 노출한다.
  • 이 flow를 검증하기 위해 어떤 가정이 필요한지 : 상품 정보, 유저 정보(여기에는 해당 유저의 기존 장바구니 내역도 포함)

이렇게 먼저 필요한 것들을 준비하고 3단계로 테스트 케이스를 작성한다.

  • step 1 : given
    • 테스트를 위해 준비하는 과정
    • 테스트에 사용하는 변수, 입력 값 정의
    • 필요시 mock 객체 정의
  • step 2 : when
    • 실제로 액션을 테스트하는 과정
    • 한 번에 하나의 메서드만 수행하는 것이 바람직
  • step 3 : then
    • 테스트를 검증하는 과정
    • 예상한 값, 실제 실행을 통해 나온 값을 검증

 

참고자료

https://jestjs.io/

https://www.daleseo.com/jest-basic/

https://jbee.io/react/testing-0-react-testing-intro/

https://brunch.co.kr/@springboot/292