[Node.js교과서] 단위 테스트하기

Donghun Seol·2023년 4월 4일
0

node.js 교과서

목록 보기
12/12

0. 알았었는데요... 몰랐습니다

분명 학습하고, 코드도 따라 쳐보고, 간단한 테스트도 작성해봤는데 머리가 하얗다. 공부를 제대로 안했다는 증거고, 결국 복습해야한다. 한번보고 두번보고 세번 정도 보고나면 기억에 남더라.

1. 테스트의 중요성

말할 필요가 있나? 처음에는 신나게 기획대로 코딩한다. 잘 돌아가는거 같아 보인다. 그런데 점점 소프트웨어가 복잡해지면서 문제가 생긴다. 하나 둘 버그가 튀어나오고 여길 고치면 저기가 터지고 저길 또 고치면 컴파일이 안된다. 예상하지 못한 의존성이 생겨서 코드가 꼬여버리면 나중엔 이러지도 저러지도 못하는 상황이 생긴다.

버그가 무슨 모듈에서 왜 발생했는지 파악하려면 디버거키고 한줄한줄 살펴봐야되는데 버그가 생길때마다 처음부터 다시봐야하면 어느세월에 개발하나? 계속 이런일이 생기면 멘탈과 자존감은 블랙홀로... 프로젝트는 안드로메다로... 날아가버린다. 그러니까 테스트를 작성하는게 나한테도 좋고 너한테도 좋고 프로덕트한테도 좋다.(아마 사장님이 제일 좋아하실거다. 개발자의 시간과 컴퓨팅 리소스는 💵.) 따라서 개발과정에서 꼼꼼하게자동화된 테스트는를 작성해나가야 한다. 테스트를 작성하면 다음과 같은 장점이 있다고 생각한다.(뇌피셜이다...)

  1. 내 코드에 대한 나의 이해 향상
  2. 개발생산성 향상
  3. 소프트웨어 품질 향상
  4. 믿을 수 있는 코드로 인한 동료들의 생산성 향상
  5. 프로젝트의 안정 및 장기적인 개선 가능

그러니 실무면접에서 테스팅에 관해 질문하는게 당연하다. 내가 시니어라면 신입한테 처음 시킬 업무가 기존 코드에 대한 테스트 작성일것 같다.

2. 테스트 준비하기

모듈 설치

npm i -D jest @types/jest

api 테스트

describe로 suite를 만들고, suite안에 it 또는 test로 개별 테스트를 작성한다.
개인적으로 it을 사용하는걸 선호하는데 문맥적으로 뒤의 디스크립션과 매끄럽게 연결되기 때문이다.

웹서버를 단위 테스팅할때는 모킹을 잘 활용해야 하는데, 컨트롤러나 미들웨어는 다른 모듈에 의존적이므로 의존성을 효과적으로 제거하기 위해 의존하는 모듈을 모킹으로 대체한다. 일반적으로 함수는jest.fn(() => <custom Return Value>) 형식으로 모킹한다.

아래는 isLoggedIn이라는 미들웨어를 테스트하는 코드다. 해당 미들웨어는 res, req, next를 인자로 받는다. 의존성을 제거하기 위해 describe 블록안에 각각의 객체와, isLoggedIn이 의존하고 있는 객체의 메서드를 모킹했다. 모킹을 통해 의존하고 있는 메서드의 반환값을 손쉽게 제어하고, 의존성을 제거해 순수한 유닛 테스트를 작성할 수 있다.

const { isLoggedIn, isNotLoggedIn } = require('./');

describe('isLoggedIn', () => {
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  };
  const next = jest.fn();
  it('should call next() when logged in', () => {
    const req = {
      isAuthenticated: jest.fn(() => true),
    };
    isLoggedIn(req, res, next);
    expect(next).toBeCalledTimes(1);
  });
  it('should throw error when not logged in', () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isLoggedIn(req, res, next);
    expect(res.status).toBeCalledWith(403);
    expect(res.send).toBeCalledWith('로그인 필요');
  });
});

DB까지 고려해야 한다면?

위의 예시는 db를 조회하지 않는 미들웨어를 대상으로 한 유닛 테스트다. db와 연동된 컨트롤러를 유닛테스트하는 것은 좀 더 복잡하다. 몽구스 모델인 User에 의존적인 컨트롤러를 테스트하기 위해선 User자체를 모킹해야 한다.

모듈 순서에 유의하자. 기존의 모듈을 jest로 랩핑해서 추가적인 기능을 추가한 후 require()한다.

다음은 실제 테스트 코드다. 일단 테스트 대상 모듈이 내부적으로 어떻게 DB모델객체를 활용하는지 파악해야 한다.
User라는 모델에 findOne이라는 메서드를 호출해서 해당 값을 변수에 저장하고 이 변수를 대상으로 addFollowing 메서드를 호출한다. 이중으로 모킹해야한다. 이 부분이 좀 까다롭다.

    const user = await User.findOne({ where: { id: req.user.id } });
    if (user) {
      // req.user.id가 followerId, req.params.id가 followingId
      await user.addFollowing(parseInt(req.params.id, 10));
      res.send('success');

따라서 테스트에 사용할 User.findOne()의 반환값은 다음과 같아야 한다.
1. 비동기 코드이므로 Promise<Partial<User>> 객체를 응답해야 한다.
2. 해당 객체는 addFollowing이란 메서드를 가지고 있어야 한다.
3. addFollowing 메서드도 비동기이므로 Promise<true>를 반환한다.
(타입스크립트의 도움을 받으면 테스트 작성도 엄청 효율적일것 같다.)

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

describe('addFollowing', () => {
  //일반적인 req객체를 현재 컨트롤러가 의존하는 부분만 명시해서 모킹했다.
  const req = {
    user: { id: 1},
    params: { id: 2},
  };
  // res객체의 메서드 중 테스트에 필요한 메서드만 모킹한다.
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  }
  // 인자로 들어가는 next()함수도 모킹한다. 나중에 이 함수의 호출여부를 대상으로 테스트가 작성된다.
  // 반환값은 사용하지 않으므로 fn()의 콜백은 비워도 될듯
  const next = jest.fn()
  
  // 개별 테스트
  test('사용자를 찾아 팔로잉을 추가하고 success를 응답해야 함', async () => {
    // 모킹객체를 셋업한다.
    User.findOne.mockReturnValue(Promise.resolve({
      addFollowing(id) {
        return Promise.resolve(true);
      }
    }));
    // 셋업된 모킹객체는 addFollowing안에서 활용된다.
    await addFollowing(req, res, next);
    // 평가한다.
    expect(res.send).toBeCalledWith('success')
  });
  
  // 실패하는 경우 테스트
  test('사용자를 찾지 못하면 res.status(404), send(no user)를 호출함', async () => {
    // 몽구스에서 해당 id를 찾지 못하면 null을 반환하므로..
    User.findOne.mockReturnValue(Promise.resolve(null))
    // 아래의 코드에서 위의 셋업된 모킹객체를 활용한다
    await addFollowing(req, res, next);
    // 유저를 찾지 못했을때 호출되어야 하는 함수들이 정상적으로 호출되는지 평가한다.
    expect(res.status).toBeCalledWith(404);
    expect(res.send).toBeCalledWith('no user');
});
                                                
profile
I'm going from failure to failure without losing enthusiasm

0개의 댓글