Node.js 심화 - 11
__________________________________________________
1-27 Service Layer 단위 테스트
=> < 3.5 테스트 코드 05 >
3.5 테스트 코드 Goal : 테스트 코드란 무엇인지 이해하고 Jest를 이용해 단위 테스트 코드를 작성할 수 있다.
폴더 : kimminsoo -> sparta -> node_js -> learning -> third_step -> layered-architecture-pattern
파일 : __tests__ -> unit -> posts.service.unit.spec.js
서비스 레이어는 사용자의 요구사항을 직접적으로 처리하는 비지니스 로직을 담당하는 파트.
DB 정보가 필요할때에는 저장소에게 요청하는 사이드 이기도.
뿐만 아니라 에러 핸들링을 담당하는 부분이기도 해. Try catch 로 잡아서 해당 블럭에 따라 다른 값을 컨트롤러에게 리턴하는 거지.
Service 레이어는 하위 계층으로서 Repository 레이어를 가지고 있지.
즉, Service 레이어의 단위 테스트를 구현하기 위해서는 Repository 계층을 Mocking 하여 원하는 비지니스 로직이
정상적으로 동작하는지 확인해야 해.
Service Layer의 단위 테스트는 findAllPost, deletePost 2개의 Method를 테스트 해보자.
성공 케이스만 검증한 Repository의 단위 테스트랑 다르게, Service에서는 에러를 고의로 발생시켜 정상적으로 에러가 발생하는지 또한 검증할 예정.
1.
__tests__ -> unit -> posts.service.unit.spec.js 파일 만들기.
——
// __tests__/unit/posts.service.unit.spec.js
const PostService = require("../../services/posts.service.js");
let mockPostsRepository = {
findAllPost: jest.fn(),
findPostById: jest.fn(),
createPost: jest.fn(),
updatePost: jest.fn(),
deletePost: jest.fn(),
}
let postService = new PostService();
// postService의 Repository를 Mock Repository로 변경합니다.
postService.postRepository = mockPostsRepository;
describe('Layered Architecture Pattern Posts Service Unit Test', () => {
// 각 test가 실행되기 전에 실행됩니다.
beforeEach(() => {
jest.resetAllMocks(); // 모든 Mock을 초기화합니다.
})
test('Posts Service findAllPost Method', async () => {
const findAllPostReturnValue = [
{
postId: 1,
nickname: "lololo",
title: "title title 1",
createdAt: new Date("11 October 2022 00:00"),
updatedAt: new Date("11 October 2022 00:00"),
},
{
postId: 2,
nickname: "lololo",
title: "title title 2",
createdAt: new Date("12 October 2022 00:00"),
updatedAt: new Date("12 October 2022 00:00"),
},
]
mockPostsRepository.findAllPost = jest.fn(() => {
return findAllPostReturnValue;
})
const allPost = await postService.findAllPost();
expect(allPost).toEqual(findAllPostReturnValue.sort((a, b) => { return b.createdAt - a.createdAt }));
expect(mockPostsRepository.findAllPost).toHaveBeenCalledTimes(1);
});
test('Posts Service deletePost Method By Success', async () => {
const findAllPostReturnValue = {
postId: 1,
nickname: "lololo",
title: "title title 1",
content: "content content",
createdAt: new Date("11 October 2022 00:00"),
updatedAt: new Date("11 October 2022 00:00")
};
mockPostsRepository.findPostById = jest.fn(() => {
return findAllPostReturnValue;
})
const deletePost = await postService.deletePost(1, "0000");
// findPostById Method 를 1번 호출한다. 입력받은 인자는 postId 이다.
expect(mockPostsRepository.findPostById).toHaveBeenCalledTimes(1);
expect(mockPostsRepository.findPostById).toHaveBeenCalledWith(1);
// postId, password를 받고 deletePost Method 가 호출된다.
expect(mockPostsRepository.deletePost).toHaveBeenCalledTimes(1);
expect(mockPostsRepository.deletePost).toHaveBeenCalledWith(1, "0000");
// Return 된 결과값이, findPostById 의 반환된 결과값과 일치한다.
expect(deletePost).toMatchObject({
postId: findAllPostReturnValue.postId,
nickname: findAllPostReturnValue.nickname,
title: findAllPostReturnValue.title,
content: findAllPostReturnValue.content,
createdAt: findAllPostReturnValue.createdAt,
updatedAt: findAllPostReturnValue.updatedAt,
});
});
test('Posts Service deletePost Method By Not Found Post Error', async () => {
const findPostByIdReturnValue = null; // postId 를 넣겠지만, 해당 게시글을 찾을 수 없다.
mockPostsRepository.findPostById = jest.fn(() => {
return findPostByIdReturnValue
});
try {
const deletePost = await postService.deletePost(3, "0000");
} catch (error) {
// 1. postId 를 입력한 findPostById 실행, 1번 호출
expect(mockPostsRepository.findPostById).toHaveBeenCalledTimes(1);
expect(mockPostsRepository.findPostById).toHaveBeenCalledWith(3);
// 2. 리턴된 findPostById의 결과가 존재하지 않을 때 에러 발생.
expect(error.message).toEqual("Post doesn't exist");
}
});
});
——
***
try catch와 같이 에러 핸들링을 하지 않고, expect.toThrow(error)와 같이 에러 핸들링 없이 에러 케이스를 검증할 수 있는 방법도 있다.
=>. 참고 : https://jestjs.io/docs/expect#tothrowerror
Service Layer의 테스트 코드를 구현하면서 성공 케이스 뿐만 아니라 에러 케이스까지 검증할 수 있는 테스트 코드를 작성할 수 있게 되었어.
__________________________________________________
1-28 Controller 단위 테스트
=> < 3.5 테스트 코드 05 >
3.5 테스트 코드 Goal : 테스트 코드란 무엇인지 이해하고 Jest를 이용해 단위 테스트 코드를 작성할 수 있다.
폴더 : kimminsoo -> sparta -> node_js -> learning -> third_step -> layered-architecture-pattern
파일 : __tests__ -> unit -> posts.controller.unit.spec.js
=> 실행 명령어는 다음과 같아.
——
npm run test:unit
——
Controller는 가장 처음으로 API가 호출되었을 때, 실행되는 계층.
Router의 경우는 API의 Path를 Parsing 하거나 Middleware를 설정하는 역할을 하고 있다면,
Controller는 Client가 전달한 요청(Request)의 유효성 검사 및 비즈니스 로직을 수행할 수 있도록 Service Layer로 전달하는 가장 첫번째 Layer.
Controller의 단위 테스트(Unit Test)는 전달된 데이터들의 유효성 검사가 정상적으로 이루어 지는 지,
특정상황에서 에러가 정상적으로 발생 하는지에 대한 테스트를 검증해 봐야 해.
Controller는 Service Layer를 하위 계층으로 가지고 있다.
그렇기 때문에, 단위 테스트 (UnitTest)를 구현하기 위해서는 의존하고 있는 하위 계층을 Mocking을 하여
Controller만을 테스트 할 수 있도록 수정해야 해.
*****
Controller의 경우 Repository, Service의 테스트 코드와 다르게 req, res, next 3가지의 입력인자를 받는다.
따라서 입력 인자들 또한 Mocking하여 원하는 Response가 전달되었는지, Http Status Code가 정상적으로 반영 되었는지를 검증할 수 있어야 한다.
——
// __tests__/unit/posts.controller.unit.spec.js
const PostsController = require("../../controllers/posts.controller.js");
// posts.service.js 에서는 아래 5개의 Method만을 사용합니다.
let mockPostService = {
findAllPost: jest.fn(),
findPostById: jest.fn(),
createPost: jest.fn(),
updatePost: jest.fn(),
deletePost: jest.fn(),
}
let mockRequest = {
body: jest.fn(),
};
let mockResponse = {
status: jest.fn(),
json: jest.fn(),
};
let postsController = new PostsController();
// postsController의 Service를 Mock Service로 변경합니다.
postsController.postService = mockPostService;
describe('Layered Architecture Pattern Posts Controller Unit Test', () => {
// 각 test가 실행되기 전에 실행됩니다.
beforeEach(() => {
jest.resetAllMocks(); // 모든 Mock을 초기화합니다.
// mockResponse.status의 경우 메서드 체이닝으로 인해 반환값이 Response(자신: this)로 설정되어야합니다.
mockResponse.status = jest.fn(() => {
return mockResponse
});
})
test('Posts Controller getPosts Method by Success', async () => {
const findAllPostReturnValue = [
{
postId: 1,
nickname: "lololo",
title: "title title 1",
createdAt: new Date("11 October 2022 00:00"),
updatedAt: new Date("11 October 2022 00:00"),
},
{
postId: 2,
nickname: "lololo",
title: "title title 2",
createdAt: new Date("12 October 2022 00:00"),
updatedAt: new Date("12 October 2022 00:00"),
},
];
mockPostService.findAllPost = jest.fn(() => {
return findAllPostReturnValue;
})
await postsController.getPosts(mockRequest, mockResponse);
// controller 는 리턴을 하지 않기에 저장하지 않을거야.
// 1. findAllPost Method 가 한 번 호출되었는가.
expect(mockPostService.findAllPost).toHaveBeenCalledTimes(1);
// 2. Response.status 가 200 코드로 정상 전달 되었는가.
expect(mockResponse.status).toHaveBeenCalledTimes(1);
expect(mockResponse.status).toHaveBeenCalledWith(200); // 리턴이 아닌 실행이라서 toEqual 로는 비교 못해.
// 3. Response.json 이 {data: posts} 상태로 잘 전달 되었는가.
expect(mockResponse.json).toHaveBeenCalledWith({data: findAllPostReturnValue});
});
test('Posts Controller createPost Method by Success', async () => {
const createPostBodyParams = {
nickname: "lololo",
password: "12344",
title: "title 2222",
content: "content 2222"
}
mockRequest.body = createPostBodyParams;
const createPostReturnValue = { // service.js 의 createPost 리턴값 참고.
postId: 1,
nickname: "lololo",
title: "title 2222",
content: "content 2222",
createdAt: new Date().toString(),
updatedAt: new Date().toString()
}
mockPostService.createPost = jest.fn(() => {
return createPostReturnValue;
});
await postsController.createPost(mockRequest, mockResponse);
// 1. Request body 데이터가 정상적으로 createPost에 전달되었는가.
expect(mockPostService.createPost).toHaveBeenCalledTimes(1);
expect(mockPostService.createPost).toHaveBeenCalledWith(
createPostBodyParams.nickname,
createPostBodyParams.password,
createPostBodyParams.title,
createPostBodyParams.content
);
// 2. mockResponse.json 을 호출하는데, createPost에서 리턴된 데이터가 맞는가.
expect(mockResponse.json).toHaveBeenCalledTimes(1);
expect(mockResponse.json).toHaveBeenCalledWith({ data: createPostReturnValue});
// 3. mockResponse.status 에 201이 정상적으로 전달되었는가.
expect(mockResponse.status).toHaveBeenCalledTimes(1);
expect(mockResponse.status).toHaveBeenCalledWith(201);
});
test('Posts Controller createPost Method by Invalid Params Error', async () => {
mockRequest.body = {};
await postsController.createPost(mockRequest, mockResponse);
// 1. mockResponse 의 status 가 400이냐
expect(mockResponse.status).toHaveBeenCalledTimes(1);
expect(mockResponse.status).toHaveBeenCalledWith(400);
// 2. mockResponse 의 json이 { errorMessage: 'InvalidParamsError' } 가 맞냐.
expect(mockResponse.json).toHaveBeenCalledTimes(1);
expect(mockResponse.json).toHaveBeenCalledWith({errorMessage: 'InvalidParamsError'});
});
});
——
Controller의 테스트 코드를 구현하면서 res, req 객체를 Mocking하여 Return 값이 아니더라도
반환된 정보를 검증할 수 있는 테스트 코드를 작성할 수 있게 되었어.