기존 JS 프로젝트에서 사용하였던 유닛 테스트 코드를 그대로 TS에서도 큰 수정 없이 동작하지 않을까 하였지만, 타입 에러를 해결하는데 꽤 애를 먹었다.
아직 TypeScript가 익숙치 않아서 인지, 생각보다 타입 지정하는데에 한참 구글링 및 삽질을 하였다.
간단한 미들웨어 로직, 유저 정보를 불러오는 컨트롤러 로직 하나씩을 긁어와 초간단 예제를 작성해보며 기록해본다.
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
return res.status(401).send('로그인이 필요합니다.');
}
}
로그인 여부를 테스트 하는 아주 간단한 미들웨어 로직이다.
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
타입을 가지는 req
와 Response
타입인 res
를 요구 한다.
테스트를 위한 req
, res
를 모킹하여 작성하는 과정에서 타입 불일치가 일어나 정상적으로 미들웨어를 호출할 수 없다.
따라서 정상적으로 테스트 코드를 실행하기 위해선 타입 불일치가 일어나는 부분들을 알맞게 타이핑 해 주어야 한다.
export const isLoggedIn = (req: Request, res: Response, next: NextFunction) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(401).send('로그인이 필요합니다.');
}
};
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
타입을 갖도록 하였다.
테스트를 실행해보면
성공적으로 테스트가 끝나게 된다.
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);
}
};
로그인이 되어 있을 시, 로그인 한 유저의 정보를 응답하는 간단한 컨트롤러 로직이다.
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 환경으로 옮겨 보면,
당연하게도 이 코드에서도 타입 에러들이 많이 발생하였고, 테스트 실행 시 그에 따른 오류가 발생한다.
마찬가지로 타입 에러들을 해결해주어야 정상적으로 코드가 동작할 것이다.
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
는 미들웨어 부분에서처럼 모킹을 해주고, 시퀄라이즈 메서드인 findOne
은 jest.fn()
을 활용하여 모킹 해준다.
테스트를 실행해보면,
성공적으로 테스트가 끝난다.