[Node.JS] 개발 후 테스트 하기 - 유닛 테스트

진실·2021년 8월 4일
0

개발을 마친 후 배포하기 전에는 반드시 테스트를 해야합니다.

노드로 만든 서버를 테스트를 할 때는 jest라는 패키지를 사용합니다.

jest는 페이스북에서 만든 테스트 패키지입니다.

package.json에 "test" 추가하기

"scripts": {
    "start": "nodemon server",
    "test": "jest",

test를 입력할때 jest가 실행되도록 package.json에 "test"를 추가합니다.

npm test를 입력하면 jest가 확장자에 test가 들어간 파일을 전부 찾아 테스트합니다.

test 함수

sum.js


function sum(a, b) {
  return a + b;
}
module.exports = sum;

sum.test.js

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

sum.js를 테스트하기 위한 코드인 sum.test.js입니다. test 함수의 첫번째 인자는 테스트에 대한 설명입니다.

두번째 인자는 테스트 함수입니다. 코드를 보면 직관적으로 함수를 이해할 수 있습니다.

2+2의 기댓값이 4라는 것입니다. 만약에 2+2가 4면 테스트를 통과하고, 4가 아니라면 테스트가 실패할 것입니다.


위 코드를 실행시키면 테스를 통과합니다.

toBe의 인자를 5로 바꾸었더니 위와 같은 테스트 실패 문구가 나옵니다.

.test(name, fn, timeout)

name : 테스트에 대한 설명

fn : 테스트 함수

테스트 함수의 안을 보면 expect라는 함수가 있습니다. expect 뒤에는 toBe 라는 함수가 있는데요, 이러한 함수를 matcher라고 합니다.

expect 함수는 단독으로 사용하는 일이 거의 없고,matcher와 함께 expect함수의 리턴값이 matcher와 일치하는지 확인하는 데 사용됩니다.

.expect(value)

value : 값. 보통 인자로 함수를 넣어 함수의 리턴값을 넘겨줍니다.

.toBe(value)

값을 비교하는 데 사용됩니다.

value : 비교할 값

describe 함수

서로 관련 있는 test들끼리 묶을 때는 describe라는 함수를 사용합니다.

const myBeverage = {
  delicious: true,
  sour: false,
};

describe('my beverage', () => {
  test('is delicious', () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test('is not sour', () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});

테스트 그룹의 이름은 my beverage입니다. 그리고 그 뒤에 함수로 test 함수들을 묶어줍니다.

첫번째로 myBeverage.delicious가 true인지 테스트합니다. 그 다음 myBeverage.sour가 false인지 테스트합니다.

.describe(name, fn)

name : 테스트 그룹의 이름

fn : 테스트 함수들의 묶음

콘솔창을 보면 my beverage라는 테스트 그룹에 속하는 테스트들이 통과됐는 지를 보여 줍니다.

mocking - mock function

forEach.js

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

위와 같은 forEach 함수가 있습니다. forEach 함수를 테스트 하기 위해서는 items와 callback 함수가 필요합니다.

위 코드를 테스트를 할 때는 실제 callback 함수가 아닌, 임시의 테스트용 함수를 만들어서 사용합니다. 이렇게 테스트용 함수나 객체 등을 만드는 것을 mocking이라고 합니다.

forEach.test.js

forEach = require('./forEach');

test('forEach()', () => {
  const mockCallback = jest.fn(x => 42 + x);
  forEach([0, 1], mockCallback);

  // The mock function is called twice
  // same as expect(mockCallback).toBeCalledTimes(2);
  expect(mockCallback.mock.calls.length).toBe(2);

  // The first argument of the first call to the function was 0
  expect(mockCallback.mock.calls[0][0]).toBe(0);

  // The first argument of the second call to the function was 1
  expect(mockCallback.mock.calls[1][0]).toBe(1);

  // The return value of the first call to the function was 42
  expect(mockCallback.mock.results[0].value).toBe(42);
})

forEach 함수의 두번째 parameter인 callback을 모킹하기 위해 mockCallback이라는 함수를 만듭니다.

함수를 모킹하기 위해서는 jest.fn()이라는 함수를 사용합니다. jest.fn(x=>42+x)는 function(x=>x+42)라는 함수를 모킹한다는 뜻입니다.

첫번째로 mockCallback 함수가 호출된 횟수를 테스트합니다. 그리고 첫번재 호출에서의 첫번째 인자가 0인지를 테스트합니다. 그 다음에는 두번째 호출에서의 첫번재 인자가 1인지를 테스트합니다. 그리고 첫번째 호출에서의 리턴값이 42인지를 테스트합니다.

위 테스트를 수행하면 통과하게 됩니다.

jest.fn(implementation)
implementation : 모킹할 내용

mockFn.mock.calls
i번째 호출에서의 j번째 인자를 갖는 배열

예를 들어 f('arg1', 'arg2')를 호출하고 f('arg3', 'arg4')를 호출하면

[
  ['arg1', 'arg2'],
  ['arg3', 'arg4'],
];

mockFn.mock.calls는 이런 배열을 갖게 됩니다.

mockFn.mock.results
type과 value라는 객체를 요소로 하는 배열

[
  {
    type: 'return',
    value: 'result1',
  },
  {
    type: 'throw',
    value: {
      /* Error instance */
    },
  },
  {
    type: 'return',
    value: 'result2',
  },
];

예를 들어 어떤 함수 f가 세번 호출됐을 때의 mockFn.mock.results 배열은 위와 같습니다.

mockFn.mockName(value)

const mockFn = jest.fn().mockName('mockedFunction');
// mockFn();
expect(mockFn).toHaveBeenCalled();

value : mock 함수의 이름
mock 함수에 jest.fn() 대신 이름을 붙일 때 사용하는 함수입니다.

mockfn.mockReturnThis()
mock 함수 자기 자신을 리턴하도록 합니다.

mockFn.mockReturnValue(value)

const mock = jest.fn();
mock.mockReturnValue(42);
mock(); // 42
mock.mockReturnValue(43);
mock(); // 43

mock 함수의 리턴값을 지정해주는 함수입니다.
value : mock 함수의 리턴값

Mocking - mock modules

jest에서는 함수 뿐만 아니라 모듈도 모킹할 수 있습니다.

// banana.js
module.exports = () => 'banana';

// __tests__/test.js
jest.mock('../banana');

const banana = require('../banana'); // banana will be explicitly mocked.

banana(); // will return 'undefined' because the function is auto-mocked.

jest.mock에 모킹할 모듈의 이름을 넘겨줍니다. 위의 예시에서는 banana.js 모듈을 모킹합니다.

그리고 banan를 require해주면 banana가 모킹이 됩니다.

jest.mock('../models/user');
const User = require('../models/user');
const {addFollowing} = require('./user');

describe('addFollowing', ()=>{
  const req = {
    user : {
      id : 1,
    },
    params : {
      id : 2,
    }
  };
  const res = {
    status : jest.fn(()=>res),
    send : jest.fn(),
  };
  const next = jest.fn();

  test('사용자를 찾아 팔로잉을 추가하고 success를 응답해야 함', async()=>{
    User.findOne.mockReturnValue({
      addFollowing(id){
        return Promise.resolve(true);
      }
    });
    await addFollowing(req, res, next);
    expect(res.send).toBeCalledWith('success');
  });

  test('사용자를 못 찾으면 res.status(404).send(no user)를 호출함', async()=>{
    User.findOne.mockReturnValue(null);
    await addFollowing(req, res, next);
    expect(res.status).toBeCalledWith(404);
    expect(res.send).toBeCalledWith('no user');
  });

  test('DB에서 에러가 발생하면 next(error)를 호출함', async()=>{ 
    const error = '테스트용 에러';
    User.findOne.mockReturnValue(Promise.reject(error));
    await addFollowing(req, res, next);
    expect(next).toBeCalledWith(error);
  });
})

코드가 깁니다. 천천히 살펴보도록 하겠습니다.

먼저 jest.mock으로 모킹할 모듈을 지정해줍니다. user모듈은 sequelize model입니다. user모듈은 init과 associate이라는 메서드를 갖고 있습니다. addFollowing은 user 모듈에 팔로잉을 추가해주는 함수입니다.

나중에 addFollowing 함수에서 req, res, next 객체가 필요하기 때문에 req, res, next 객체를 만들어줍니다.

제일 첫번째로 사용자를 찾아서 팔로잉을 추가하고 success를 응답하는 부분을 테스트합니다.

sequelize의 쿼리는 promise 기반이기 때문에 promise를 resolve하거나 rejct 해줘야 합니다.

먼저 mockReturnValue를 통해 User.findOne의 리턴값을 { addFollwing() } 객체로 지정해 주었습니다. 사용자를 찾은 다음 팔로잉을 추가하는 상황을 테스트하기 위해서입니다.

그리고 실제로 테스트를 하기 위해 addFollowing 함수를 호출해서 테스트해봅니다. res.send가 'success' 인자와 함께 호출되는지 테스트합니다.

다음은 사용자를 못 찾은 경우에 대해 테스트합니다.

사용자를 못 찾은 경우에는 팔로잉을 추가하지도 않으므로 User.findOne의 리턴값을 null로 지정해줍니다. 그리고 addFollowing 함수를 테스트합니다. 이 경우에는 res.status가 '404' 인자와 함께 호출되고, res.send 함수가 'no user'와 함께 호출되는지 테스트합니다.

마지막으로 사용자를 찾다가 에러가 발생한 경우입니다.

이때는 User.findOne의 리턴값을 Promise.reject(error)로 지정해줍니다. 그리고 addFollwing을 호출해서 next가 error와 함께 호출됐는지를 테스트합니다.

정리

기본적으로는 test(name, fn) 함수를 이용해서 테스트를 합니다. 비슷한 여러 테스트를 그룹화 할때는 describe(name, implementation) 함수를 사용해서 묶어줍니다.

expect 함수와 toBe, toBeCalledWith등의 matcher를 사용해서 테스트하는 부분이 올바르게 작동하는지 테스트합니다.

테스트하는 함수의 parameter나 함수 내부에서 또다른 함수나 모듈을 사용하는 경우가 있습니다. 이러한 경우에는 테스트하는 범위에 맞게 적절히 mocking을 해줍니다. jest에서는 jest.fn(), jest.mock() 등의 다양한 모킹 메서드가 있으므로 이를 활용합니다.

코드를 테스트를 하기 위해서는 내가 작성한 코드에 대한 이해가 충분해야 합니다. 각 함수에서 사용되는 변수, 객체, 함수 등이 무엇이 있는 지를 명확히 파악해야 합니다. 그리고 테스트 케이스마다 올바른 리턴값이 무엇인지, 오류가 발생했을 경우 코드가 어떻게 대처하는 지를 알아야합니다. 그리고 적절한 리턴값을 지정해주기 위해서는 내가 사용하는 함수가 promise 기반인지도 알고 있어야 합니다.

해당 언어에 대한 숙련도가 떨어질수록, 테스트코드를 작성하는 데 시간이 오래걸린다는 글을 본 적이 있습니다. 빠르게 적절한 테스트 코드를 작성하기 위해서는 우선 그 언어에 대한 이해가 필수적이라고 생각됩니다.

profile
반갑습니다.

0개의 댓글