본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
(작성한 코드의 양이 너무 많아 이력서 지원 상태 변경 기능에 대한 테스트 코드로 설명)
이력서 지원상태 변경 기능은 Recruiter가 지원자의 이력서에 대한 평가를 내리는 기능임
이 기능은 다른 기능들과 다르게 2가지 종류의 테이블에 대한 접근이 같이 이뤄져야 함
그렇기에 Prisma의 Transaction문법을 사용해서 한 트랜젝션에서 2가지 일이 처리되도록 구현함
마찬가지로 테스트 코드에서도 트랜젝션이 동작하는 것처럼 구현해줘야 함
이력서 지원상태 변경 실행 코드
// src/repositories/resume.repository.js
// 이력서 상태 변경
updateResumeState = async (userId, resumeId, oldState, newState, reason) => {
let resumeLog; // 이력서 변경 로그
// 트랜젝션을 통해서 작업의 일관성 유지
await this.prisma.$transaction(
async (tx) => {
// 이력서 수정
await tx.resume.update({ where: { resumeId: +resumeId }, data: { state: newState } });
// 이력서 변경 로그 생성
resumeLog = await tx.resumeHistory.create({
data: { recruiterId: +userId, resumeId, oldState, newState, reason },
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
},
);
return resumeLog;
};
// __tests__/unit/repositories/resume.repository.unit.spec.js
const mockPrisma = {
resume: {
create: jest.fn(),
findMany: jest.fn(),
findFirst: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
resumeHistory: {
create: jest.fn(),
findMany: jest.fn(),
},
$transaction: jest.fn(),
};
...
// 이력서 상태 변경
test('updateResumeState Method', async () => {
/* 설정 부분 */
// Resume Repository의 updateResumeState 메서드 매개변수 임시값
const updateResumeStateParams = {
userId: 2,
resumeId: 22,
oldState: RESUME_CONSTANT.RESUME_STATE.APPLY,
newState: RESUME_CONSTANT.RESUME_STATE.PASS,
reason: '서류 통과',
};
// 모킹된 Prisma의 update 메서드의 임시 반환값
const resumeLogSample = {
resumeLogId: 7,
resumeId: updateResumeStateParams.resumeId,
recruiterId: updateResumeStateParams.userId,
oldState: RESUME_CONSTANT.RESUME_STATE.APPLY,
newState: RESUME_CONSTANT.RESUME_STATE.PASS,
reason: updateResumeStateParams.reason,
createdAt: new Date(),
};
// 모킹된 Prisma의 create 메서드의 결과값 설정
mockPrisma.resumeHistory.create.mockReturnValue(resumeLogSample);
// 모킹된 Prisma의 $transaction 메서드의 결과값 설정
mockPrisma.$transaction.mockImplementation(async (tx) => {
return await tx(mockPrisma);
});
/* 실행 부분, 실제 Resume Repository의 updateResumeState 메서드 실행 */
const resumeLog = await resumeRepository.updateResumeState(
updateResumeStateParams.userId,
updateResumeStateParams.resumeId,
updateResumeStateParams.oldState,
updateResumeStateParams.newState,
updateResumeStateParams.reason,
);
/* 테스트(조건) 부분 */
// 모킹된 Prisma $transaction 메서드가 1번만 실행되었는지 검사
expect(mockPrisma.$transaction).toHaveBeenCalledTimes(1);
// 모킹된 Prisma resume의 update 메서드가 1번만 실행되었는지 검사
expect(mockPrisma.resume.update).toHaveBeenCalledTimes(1);
// 모킹된 Prisma resume의 update 메서드가 매개변수와 함께 호출되었는지 검사
expect(mockPrisma.resume.update).toHaveBeenCalledWith({
where: { resumeId: updateResumeStateParams.resumeId },
data: { state: updateResumeStateParams.newState },
});
// 모킹된 Prisma resume의 create 메서드가 1번만 실행되었는지 검사
expect(mockPrisma.resumeHistory.create).toHaveBeenCalledTimes(1);
// 모킹된 Prisma resume의 create 메서드가 매개변수와 함께 호출되었는지 검사
expect(mockPrisma.resumeHistory.create).toHaveBeenCalledWith({
data: {
recruiterId: resumeLogSample.recruiterId,
resumeId: resumeLogSample.resumeId,
oldState: resumeLogSample.oldState,
newState: resumeLogSample.newState,
reason: resumeLogSample.reason,
},
});
// updateResumeState 메서드의 실행 결과값과
// 모킹된 Prisma resumeHistory의 create 메서드의 결과값이 같은지 검사
expect(resumeLog).toEqual(resumeLogSample);
});
// src/services/resume.service.js
// 이력서 상태 변경
updateResumeState = async (userId, resumeId, newState, reason) => {
const whereCondition = { resumeId };
// 이력서 ID가 일치한 이력서 조회
const resume = await this.resumeRepository.getResumeDetail(whereCondition);
if (!resume) throw new HttpError.NotFound(MESSAGES.RESUMES.COMMON.NOT_FOUND);
const resumeLog = await this.resumeRepository.updateResumeState(
userId,
resumeId,
resume.state,
newState,
reason,
);
return resumeLog;
};
// __tests__/unit/services/resume.service.unit.spec.js
// 이력서 상태 변경
test('updateResumeState Method', async () => {
/* 설정 부분 */
// Resume Service의 updateResumeState 메서드 매개변수 임시값
const updateResumeStateParams = {
userId: 1,
resumeId: 12,
newState: RESUME_CONSTANT.RESUME_STATE.PASS,
reason: '서류 통과',
};
// Resume Repository의 updateResume 메서드의 임시 반환값
const resumeSample = {
resumeId: 12,
userId: 1,
title: '스파르탄 자기소개',
introduce: '열심히 화이팅 화이팅!! ',
state: 'APPLY',
createdAt: '2024-06-14T07:15:16.397Z',
updatedAt: '2024-06-14T07:15:16.397Z',
};
// Resume Repository의 getResumeDetail 메서드의 결과값 설정
mockResumeRepository.getResumeDetail.mockReturnValue(resumeSample);
// Resume Repository의 updateResumeState 메서드의 임시 반환값
const resumeLogSample = {
resumeLogId: 7,
resumeId: updateResumeStateParams.resumeId,
recruiterId: updateResumeStateParams.userId,
oldState: resumeSample.state,
newState: updateResumeStateParams.newState,
reason: updateResumeStateParams.reason,
createdAt: new Date(),
};
// Resume Repository의 updateResumeState 메서드의 결과값 설정
mockResumeRepository.updateResumeState.mockReturnValue(resumeLogSample);
/* 실행 부분, 실제 Resume Service의 updateResumeState 메서드 실행 */
const resumeLog = await resumeService.updateResumeState(
updateResumeStateParams.userId,
updateResumeStateParams.resumeId,
updateResumeStateParams.newState,
updateResumeStateParams.reason,
);
/* 테스트(조건) 부분 */
// Resume Repository의 getResumeDetail 메서드가 1번만 실행되었는지 검사
expect(mockResumeRepository.getResumeDetail).toHaveBeenCalledTimes(1);
// Resume Repository의 getResumeDetail 메서드가 매개변수와 함께 호출되었는지 검사
expect(mockResumeRepository.getResumeDetail).toHaveBeenCalledWith({
resumeId: updateResumeStateParams.resumeId,
});
// Resume Repository의 updateResumeState 메서드가 1번만 실행되었는지 검사
expect(mockResumeRepository.updateResumeState).toHaveBeenCalledTimes(1);
// Resume Repository의 updateResumeState 메서드가 매개변수와 함께 호출되었는지 검사
expect(mockResumeRepository.updateResumeState).toHaveBeenCalledWith(
updateResumeStateParams.userId,
updateResumeStateParams.resumeId,
resumeSample.state,
updateResumeStateParams.newState,
updateResumeStateParams.reason,
);
// Resume Service의 updateResumeState 메서드 실행 결과값과
// Resume Repository의 updateResumeState 메서드의 결과값이 같은지 검사
expect(resumeLog).toEqual(resumeLogSample);
});
// src/controllers/resume.controller.js
// 이력서 지원 상태 변경 기능
updateResumeState = async (req, res, next) => {
try {
// 사용자 정보 가져옴
const { userId } = req.user;
// 이력서 ID 가져옴
const { resumeId } = req.params;
//지원 상태, 사유 가져옴
const { state, reason } = req.body;
// 이력서 상태 변경
const resumeLog = await this.resumeService.updateResumeState(userId, +resumeId, state, reason);
return res
.status(HTTP_STATUS.CREATED)
.json({ status: HTTP_STATUS.CREATED, message: MESSAGES.RESUMES.STATE.SUCCEED, data: { resumeLog } });
} catch (err) {
next(err);
}
};
// __tests__/unit/controllers/resume.controller.unit.spec.js
// 이력서 상태 변경
test('updateResumeState Method', async () => {
/* 설정 부분 */
// Resume Controller의 updateResumeState 메서드가 실행되기 위한 Body 입력값
const updateResumeStateRequestBodyParams = {
state: RESUME_CONSTANT.RESUME_STATE.PASS,
reason: '서류 합격!!',
};
// Request의 body에 입력할 인자값 설정
mockRequest.body = updateResumeStateRequestBodyParams;
// Resume Controller의 updateResumeState 메서드가 실행되기 위한 params 입력값
const updateResumeStateRequestParams = {
resumeId: 1,
};
// Request의 params에 입력할 인자값 설정
mockRequest.params = updateResumeStateRequestParams;
// Resume Controller의 updateResumeState 메서드가 실행되기 위한 User 입력값
const updateResumeStateRequestUserParams = {
userId: 1,
email: 'spartan@spartacodingclub.kr',
name: '스파르탄',
age: 28,
gender: 'MALE',
role: 'APPLICANT',
profileImage: 'https://prismalens.vercel.app/header/logo-dark.svg',
createdAt: '2024-06-13T08:53:46.951Z',
updatedAt: '2024-06-13T08:53:46.951Z',
};
// Request의 user에 입력할 인자값 설정
mockRequest.user = updateResumeStateRequestUserParams;
// Resume Controller의 updateResumeState 메서드의 임시 반환값
let resumeLogSample = {
resumeLogId: 4,
userName: '스파르탄',
resumeId: 11,
oldState: 'APPLY',
newState: 'PASS',
reason: '서류 심사 통과!',
createdAt: new Date(),
};
// Resume Service의 updateResumeState 메서드 반환값을 설정
mockResumeService.updateResumeState.mockResolvedValue(resumeLogSample);
/* 실행 부분, Controller의 updateResumeState 메서드 실행 */
await resumeController.updateResumeState(mockRequest, mockResponse, mockNext);
/* 테스트(조건) 부분 */
// Resume Service의 updateResumeState 메서드가 1번만 실행되었는지 검사
expect(mockResumeService.updateResumeState).toHaveBeenCalledTimes(1);
// Resume Service의 updateResumeState 메서드가 매개변수와 함께 호출되었는지 검사
expect(mockResumeService.updateResumeState).toHaveBeenCalledWith(
updateResumeStateRequestParams.resumeId,
updateResumeStateRequestUserParams.userId,
updateResumeStateRequestBodyParams.state,
updateResumeStateRequestBodyParams.reason,
);
// Response의 status 메서드가 1번만 실행되었는지 검사
expect(mockResponse.status).toHaveBeenCalledTimes(1);
// Response의 status 메서드가 매개변수와 함께 호출되었는지 검사
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
// Response의 json 메서드가 1번만 실행되었는지 검사
expect(mockResponse.json).toHaveBeenCalledTimes(1);
// Response의 json 메서드가 매개변수와 함께 호출되었는지 검사
expect(mockResponse.json).toHaveBeenCalledWith({
status: HTTP_STATUS.CREATED,
message: MESSAGES.RESUMES.STATE.SUCCEED,
data: { resumeLog: resumeLogSample },
});
});
해설 강의 영상이 빨리 나와서 강의를 시청하면서 코드를 리팩토링함
해설 강의를 보니 내가 테스트 코드에서 힘들어 했던 이유가 나왔음
나는 3계층 분리를 할 때 주요 기능별 메서드로 나눈 것이 아니라 모든 기능별로 세세하게 나눴음
그래서 테스트 코드 작성시 작성해야 하는 테스트 코드의 양이 많았고 더 복잡해 졌음
해설 강의 시청을 끝내고 나는 모든 코드들을 리팩토링했고, 구현하지 못한 이력서 테스트 코드도 작성을 완료함