TypeScript + Express + Jest 유닛 테스팅

필영·2021년 1월 16일
0
post-thumbnail

기존 JS 프로젝트에서 사용하였던 유닛 테스트 코드를 그대로 TS에서도 큰 수정 없이 동작하지 않을까 하였지만, 타입 에러를 해결하는데 꽤 애를 먹었다.

아직 TypeScript가 익숙치 않아서 인지, 생각보다 타입 지정하는데에 한참 구글링 및 삽질을 하였다.
간단한 미들웨어 로직, 유저 정보를 불러오는 컨트롤러 로직 하나씩을 긁어와 초간단 예제를 작성해보며 기록해본다.

Middleware 유닛 테스트

👉🏻 JavaScript 코드

✔️ middlewares.js

exports.isLoggedIn = (req, res, next) => {
  if (req.isAuthenticated()) {
    next();
  } else {
    return res.status(401).send('로그인이 필요합니다.');
  }
}

로그인 여부를 테스트 하는 아주 간단한 미들웨어 로직이다.

✔️ middlewares.spec.js

describe('isLoggedIn', () => {
  const res = {
    status: jest.fn(() => res),
    send: jest.fn()
  }
  
  const next = jest.fn();

  it('로그인이 되어있으면 isLoggedIn()은 next()를 호출한다..', () => {
    const req = {
      isAuthenticated: jest.fn(() => true)
    }
    isLoggedIn(req, res, next);
    expect(next).toBeCalledTimes(1);
  });

  it('로그인이 되어 있지 않으면 401 응답코드를 반환한다.', () => {
    const req = {
      isAuthenticated: jest.fn(() => false)
    }
    isLoggedIn(req, res, next);
    expect(res.status).toBeCalledWith(401);
    expect(res.send).toBeCalledWith('로그인이 필요합니다.');
  })
});

이 코드를 TS 환경으로 옮겨보았다.

역시나 보이지 않던 타입 에러들이 발생한다.
isLoggedIn()는 Request 타입을 가지는 reqResponse 타입인 res를 요구 한다.
테스트를 위한 req, res를 모킹하여 작성하는 과정에서 타입 불일치가 일어나 정상적으로 미들웨어를 호출할 수 없다.

따라서 정상적으로 테스트 코드를 실행하기 위해선 타입 불일치가 일어나는 부분들을 알맞게 타이핑 해 주어야 한다.



👉🏻 TypeScript 코드

✔️ middlewares.ts

export const isLoggedIn = (req: Request, res: Response, next: NextFunction) => {
  if (req.isAuthenticated()) {
    next();
  } else {
    res.status(401).send('로그인이 필요합니다.');
  }
};

✔️ middlewares.spec.ts

import { Request, Response } from 'express';
import { isLoggedIn } from './middlewares';

const mockResponse = (): Response => {
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  } as unknown;
  return res as Response;
};

const mockRequest = (value: boolean): Request => {
  const req = {
    isAuthenticated: jest.fn(() => value),
  } as unknown;
  return req as Request;
};

describe('isLoggedIn', () => {
  const res = mockResponse();

  const next = jest.fn();

  it('로그인이 되어있으면 isLoggedIn()은 next()를 호출한다..', () => {
    const req = mockRequest(true);
    isLoggedIn(req, res, next);
    expect(next).toBeCalledTimes(1);
  });

  it('로그인이 되어 있지 않으면 401 응답코드를 반환한다.', () => {
    const req = mockRequest(true);
    isLoggedIn(req, res, next);
    expect(res.status).toBeCalledWith(401);
    expect(res.send).toBeCalledWith('로그인이 필요합니다.');
  });
});

req, res를 모킹하여 리턴 해주는 함수를 따로 작성하였다.
타입 단언하여 return 함으로써 테스트에 사용 될 req, res가 각각 Request, Response 타입을 갖도록 하였다.

테스트를 실행해보면

성공적으로 테스트가 끝나게 된다.




컨트롤러 유닛 테스트

👉🏻 JavaScript 코드

✔️ user.js

export const getMe = async (
  req: Request,
  res: Response,
  next: NextFunction
): Promise<Response<User> | undefined> => {
  try {
    if (req.user) {
      const user = await User.findOne({
        where: { id: req.user.id },
        attributes: {
          exclude: ['password'],
        },
      });
      return res.status(200).send(user);
    }
    return res.status(200).send(null);
  } catch (err) {
    next(err);
  }
};

로그인이 되어 있을 시, 로그인 한 유저의 정보를 응답하는 간단한 컨트롤러 로직이다.

✔️ user.spec.js

describe('getMe', () => {
  const next = jest.fn();
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  };

  it('로그인이 되어있을 시 (req.user 존재), 내 유저 정보를 반환한다', async () => {
    const req = {
      user: {
        id: 1,
      },
    };

    const user = {
      id: 1,
    };

    User.findOne.mockResolvedValue(user);
    await getMe(req, res, next);
    
    expect(User.findOne).toHaveBeenCalledTimes(1);
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.send).toHaveBeenCalledWith(user);
  });

  it('로그인이 되어 있지 않으면, null을 반환한다.', async () => {
    const req = {};

    await getMe(req, res, next);
    
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.send).toHaveBeenCalledWith(null);
  });

  it('에러 발생시 next()의 인자로 err를 전달한다.', async () => {
    const req = {
      user: {
        id: 1,
      },
    };

    const err = new Error();
    User.findOne.mockRejectedValue(err);
    await getMe(req, res, next);

    expect(next).toHaveBeenCalledWith(err);
  });
});

이 코드도 TS 환경으로 옮겨 보면,

당연하게도 이 코드에서도 타입 에러들이 많이 발생하였고, 테스트 실행 시 그에 따른 오류가 발생한다.

마찬가지로 타입 에러들을 해결해주어야 정상적으로 코드가 동작할 것이다.


👉🏻 TypeScript 코드

✔️ user.spec.ts

import { Request, Response } from 'express';
import User from '../models/user';
import { getMe } from './user';

const mockResponse = (): Response => {
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  } as unknown;
  return res as Response;
};

describe('getMe', () => {
  const next = jest.fn();
  const res = mockResponse();
  const mockRequest = (): Request => {
    const req = {
      user: {
        id: 1,
      },
    } as unknown;
    return req as Request;
  };

  it('로그인이 되어있을 시 (req.user 존재), 내 유저 정보를 반환한다', async () => {
    const req = mockRequest();

    const mockUser = {
      id: 1,
    };

    User.findOne = jest.fn().mockResolvedValue(mockUser);
    await getMe(req, res, next);

    expect(User.findOne).toHaveBeenCalledTimes(1);
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.send).toHaveBeenCalledWith(mockUser);
  });

  it('로그인이 되어 있지 않으면, null을 반환한다.', async () => {
    const mockRequest = (): Request => {
      const req = {} as unknown;
      return req as Request;
    };
    const req = mockRequest();

    await getMe(req, res, next);

    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.send).toHaveBeenCalledWith(null);
  });

  it('에러 발생시 next()의 인자로 err를 전달한다.', async () => {
    const req = mockRequest();

    const err = new Error();
    User.findOne = jest.fn().mockRejectedValue(err);
    await getMe(req, res, next);

    expect(next).toHaveBeenCalledWith(err);
  });
});

req, res는 미들웨어 부분에서처럼 모킹을 해주고, 시퀄라이즈 메서드인 findOnejest.fn()을 활용하여 모킹 해준다.


테스트를 실행해보면,

성공적으로 테스트가 끝난다.

0개의 댓글