본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
테스트 코드는 말 그대로 테스트하기 위해 작성하는 코드를 뜻함
어떠한 언어에 얽매이지 않고 개념적으로 표현하기도 함
또한 코드의 에러를 확인하고 추후 나타날 수 있는 이슈와 버그를 예측하는 과정이라고 할 수 있음
https://ssowonny.medium.com/설마-아직도-테스트-코드를-작성-안-하시나요-b54ec61ef91a
테스팅은 결함이 없는 것이 아니라, 결함의 존재를 보여주는 것이다.
단위 테스트 (Unit Test) : 가장 작은 규모의 기능을 테스트 ex)서비스 단위
통합 테스트 (Integration Test) : 다양한 기능을 합쳤을 때 생기는 문제를 방지하기 위한 테스트 ex) 컨트롤러 + 서비스 단위
E2E 테스트 (End-to-End Test) : 종단 간을 의미하는 End to End 테스트 ex) 백엔드부터 웹페이지까지
# DevDependencies로 jest, cross-env 를 설치합니다.
yarn add -D jest cross-env @jest/globals
// jest.config.js
export default {
// 해당 패턴에 일치하는 경로가 존재할 경우 테스트를 하지 않고 넘어갑니다.
testPathIgnorePatterns: ['/node_modules/'],
// 테스트 실행 시 각 TestCase에 대한 출력을 해줍니다.
verbose: true,
};
// package.json
{
...
"scripts": {
...
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --forceExit",
"test:silent": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --silent --forceExit",
"test:coverage": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --coverage --forceExit",
"test:unit": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest __tests__/unit --forceExit"
},
...
}
NODE_ENV=test
형식으로 환경 변수를 설정하지만, Windows에서는 set NODE_ENV=test
로 설정해야 함--forceExit
: 테스트 코드가 완료되었을 때, 강제로 Jest 종료 (서버와 Pirsma의 연결이 남아있어 테스트 코드가 종료되지 않을 때 사용)
--silent
: 테스트 코드 실행 시, console.log
와 같은 메세지 출력하지 않음
--coverage
: 테스트 코드 완료 후 현재 프로젝트의 테스트 코드 완료 퍼센트를 출력
--verbose
: 테스트 코드의 개별 테스트 결과를 출력해줌
.mockReturnValue(value)
: Mock 함수의 반환값을 설정
.toBe(value)
: 입력받은 예상값과 결과값이 일치하는지 비교 (엄격하게 비교)
.toEqual(value)
: 입력받은 예상값과 결과값이 일치하는지 비교
.toMatch(regexp | string)
: 입력받은 예상값이 결과값과 같은지 검증
.toBeTruthy()
: 결과값이 true
인지 검증
.toBeInstanceOf(Class)
: 입력받은 예상값과 Class가 동일한지 검증 (주로 Error 검증에 사용)
.toHaveProperty(keyPath, value?)
: 입력받은 객체의 Key와 Value가 일치하는지 검증
.toMatchObject(object)
: 입력받은 객체와 결과 객체가 일치하는지 검증
afterAll(fn, timeout)
: 모든 test()
가 완료된 이후에 수행
afterEach(fn, timeout)
: 각 test()
가 완료된 이후에 수행
beforeAll(fn, timeout)
: 모든 test()
가 실행되기 전에 수행
beforeEach(fn, timeout)
: 각 test()
가 실행되기 전에 수행
Mocking
이란 모조품(Mock)을 만드는 것을 의미함
함수나 객체, 클래스의 모조품을 만들어서 실행 흐름대로 돌아가게 만드는 것
즉, 단위 테스트를 작성할 때, 해당 코드가 의존하는 부분을 가짜(mock)로 대체하는 기법
테스트 하고싶은 기능이 다른 기능들과 엮여있을 경우(의존하는 경우) 정확한 테스트를 하기 힘들기 때문에 Mocking을 사용
만약에 Repository Layer에서 테스트 코드를 실행할 때마다 DB에 접근하면 매번 새로운 데이터가 생성, 수정, 삭제되어 리소스가 낭비됨
그래서 실제 DB에 접근하지 않아도 동일한 상황을 예상하기 위해 DB에 접근하는 함수를 Mocking하면 간단하게 테스트가 가능함
.toHaveBeenCalledTimes(number)
: Mock이 몇 번 호출되었는지 검증
.toHaveBeenCalledWith(arg1, arg2, ...)
: 어떤 인자를 이용해 Mock이 호출되었는지 검증
의존성 주입(DI: Dependency Injection)은 객체 사이의 의존 관계를 외부에서 제공하는 방법을 의미함
만약 아래와 같이 Prisma 클라이언트를 직접 사용하면 findMany
와 같은 메서드로 직접 DB에 접근할 수 있음
// src/repositories/posts.repository.js
import { prisma } from '../utils/prisma/index.js';
export class PostsRepository {
findAllPosts = async () => {
// ORM인 Prisma에서 Posts 모델의 findMany 메서드를 사용해 데이터를 요청합니다.
const posts = await prisma.posts.findMany();
return posts;
};
...
}
그래서 Prisma 클라이언트에 직접 의존하는 게 아니라 외부에서 Mocking된 Prisma 클라이언트를 주입받아서 사용하면 됨
이러한 행위를 의존성 주입이라고 하고, 밑의 예제는 생성자 주입이라는 방법을 통해서 작성된 예제임
생성자 주입은 객체의 생성자(Constructor)를 호출할 때, 의존성을 전달하는 방식을 의미함
// src/repositories/posts.repository.js
export class PostsRepository {
constructor(prisma) {
// 생성자에서 전달받은 Prisma 클라이언트의 의존성을 주입합니다.
this.prisma = prisma;
}
findAllPosts = async () => {
// ORM인 Prisma에서 Posts 모델의 findMany 메서드를 사용해 데이터를 요청합니다.
const posts = await this.prisma.posts.findMany();
return posts;
};
...
}
// src/services/posts.service.js
export class PostsService {
constructor(postsRepository) {
// 생성자에서 전달받은 PostsRepository 의존성을 주입합니다.
this.postsRepository = postsRepository;
}
...
}
// src/controllers/posts.controller.js
export class PostsController {
constructor(postsService) {
// 생성자에서 전달받은 PostsService의 의존성을 주입합니다.
this.postsService = postsService;
}
...
}
// src/routes/posts.router.js
import express from 'express';
import { prisma } from '../utils/prisma/index.js';
import { PostsRepository } from '../repositories/posts.repository.js';
import { PostsService } from '../services/posts.service.js';
import { PostsController } from '../controllers/posts.controller.js';
const router = express.Router();
// 3계층의 의존성을 모두 주입합니다.
const postsRepository = new PostsRepository(prisma);
const postsService = new PostsService(postsRepository);
const postsController = new PostsController(postsService);
...
라우터들을 관리하는 최상단 계층인 posts.router.js에서 모든 계층의 의존성을 관리함
단위 테스트를 할 때 주로 데이터베이스와 가까운 Repository Layer(Repository)부터 작성함
Repository(저장소)는 데이터베이스 관리 및 데이터의 CRUD 작업을 담당함
Repository Layer가 사용하는 DB를 Mocking하여 실제 DB에 접근하지 않도록 테스트 코드를 아래와 같이 작성함
// src/repositories/posts.repository.js
// 실제로 데이터베이스에 접근해서 데이터를 CRUD하는 클래스
export class PostsRepository {
constructor(prisma) {
this.prisma = prisma;
}
// 모든 게시물 조회 메서드
findAllPosts = async () => {
const posts = await this.prisma.posts.findMany();
return posts;
};
...
// 게시물 생성 메서드
createPost = async (nickname, password, title, content) => {
const createdPost = await this.prisma.posts.create({
data: {
nickname,
password,
title,
content,
},
});
return createdPost;
};
...
// __tests__/unit/repositories/posts.repository.unit.spec.js
import { expect, jest } from '@jest/globals';
import { PostsRepository } from '../../../src/repositories/posts.repository.js';
// Prisma 클라이언트에서는 아래 5개의 메서드만 사용
let mockPrisma = {
posts: {
findMany: jest.fn(),
findFirst: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
};
let postsRepository = new PostsRepository(mockPrisma);
describe('Posts Repository Unit Test', () => {
// 각 test가 실행되기 전에 실행
beforeEach(() => {
jest.resetAllMocks(); // 모든 Mock을 초기화
});
test('findAllPosts Method', async () => {
/* 설정 부분 */
// Mocking된 Prisma 클라이언트의 findMany 메서드의 반환값 (임시 결과값)
const mockReturn = 'findMany String';
// Mocking된 Prisma 클라이언트의 findMany 메서드가 실행됐을 때, 반환되는 값 설정
mockPrisma.posts.findMany.mockReturnValue(mockReturn);
/* 실행 부분, 실제 저장소(Repository)의 findAllPosts 메서드 실행 */
const posts = await postsRepository.findAllPosts();
/* 테스트(조건) 부분 */
// 저장소(Repository)의 findAllPosts 메서드의 반환값 posts와
// Mocking된 Prisma 클라이언트의 findMany 메서드의 반환값 mockReturn과 같은지 검사
expect(posts).toBe(mockReturn);
// Mocking된 Prisma 클라이언트의 findMany 메서드가 1번만 실행되는지 검사
expect(postsRepository.prisma.posts.findMany).toHaveBeenCalledTimes(1);
});
test('createPost Method', async () => {
/* 설정 부분 */
// Mocking된 Prisma 클라이언트의 create 메서드의 반환값 (임시 결과값)
const mockReturn = 'create Post Return String';
// Mocking된 Prisma 클라이언트의 create 메서드가 실행됐을 때, 반환되는 값 설정
mockPrisma.posts.create.mockReturnValue(mockReturn);
// 저장소(Repository)의 createPost 메서드를 실행하기 위한 임시 전달 데이터 설정
const createPostParams = {
nickname: 'createPostNickname',
password: 'createPostPassword',
title: 'createPostTitle',
content: 'createPostContent',
};
/* 실행 부분, 실제 저장소(Repository)의 createPost 메서드 실행 */
const createPostData = await postsRepository.createPost(
createPostParams.nickname,
createPostParams.password,
createPostParams.title,
createPostParams.content
);
/* 테스트(조건) 부분 */
// 저장소(Repository)의 createPost 메서드 반환값 createPostData와
// Mocking된 Prisma 클라이언트의 create 메서드 반환값 mockReturn과 같은지 검사
expect(createPostData).toEqual(mockReturn);
// Mocking된 Prisma 클라이언트의 create 메서드는 1번만 실행되는지 검사
expect(mockPrisma.posts.create).toHaveBeenCalledTimes(1);
// Mocking된 Prisma 클라이언트의 create 메서드를 실행할 때
// nickname, password, title, content 데이터를 전달하는 지 검사
expect(mockPrisma.posts.create).toHaveBeenCalledWith({
data: {
nickname: createPostParams.nickname,
password: createPostParams.password,
title: createPostParams.title,
content: createPostParams.content,
},
});
});
});
Service Layer는 비즈니스 로직, 에러 핸들링과 같은 핵심적인 작업을 수행하는 곳
데이터베이스의 데이터가 필요할 때는 Repository에게 요청함
저장소(Repository)를 Mocking해서 원하는 비즈니스 로직이 동작하는지 테스트
또한 고의로 에러를 발생시켜서 정상적으로 에러가 발생하는지도 테스트
// src/services/posts.service.js
// 핵심적인 비즈니스 로직을 수행하는 클래스
export class PostsService {
constructor(postsRepository) {
this.postsRepository = postsRepository;
}
// 모든 게시물 조회 메서드
findAllPosts = async () => {
const posts = await this.postsRepository.findAllPosts();
// 게시글을 생성 날짜로 부터 내림차순 정렬
posts.sort((a, b) => {
return b.createdAt - a.createdAt;
});
// password, content를 뺀 상태로, Controller에게 Response를 전달
return posts.map((post) => {
return {
postId: post.postId,
nickname: post.nickname,
title: post.title,
createdAt: post.createdAt,
updatedAt: post.updatedAt,
};
});
};
...
// 게시물 삭제 메서드
deletePost = async (postId, password) => {
// postId에 해당하는 게시물이 있는지 체크
const post = await this.postsRepository.findPostById(postId);
if (!post) throw new Error('존재하지 않는 게시글입니다.');
await this.postsRepository.deletePost(postId, password);
return {
postId,
nickname: post.nickname,
title: post.title,
content: post.content,
createdAt: post.createdAt,
updatedAt: post.updatedAt,
};
};
}
// __tests__/unit/services/posts.service.unit.spec.js
import { expect, jest } from '@jest/globals';
import { PostsService } from '../../../src/services/posts.service.js';
// PostsRepository는 아래의 5개 메서드만 지원
let mockPostsRepository = {
findAllPosts: jest.fn(),
findPostById: jest.fn(),
createPost: jest.fn(),
updatePost: jest.fn(),
deletePost: jest.fn(),
};
// postsService의 Repository를 Mock Repository로 의존성을 주입
let postsService = new PostsService(mockPostsRepository);
describe('Posts Service Unit Test', () => {
// 각 test가 실행되기 전에 실행
beforeEach(() => {
jest.resetAllMocks(); // 모든 Mock을 초기화
});
test('findAllPosts Method', async () => {
/* 설정 부분 */
// 저장소(Repository)의 findAllPosts 메서드의 반환값 (임시 결과값)
const samplePosts = [
{
postId: 2,
nickname: '홍길동1',
title: '제목1 테스트입니다.',
createdAt: '2024-06-11T03:58:33.994Z',
updatedAt: '2024-06-11T03:58:33.994Z',
},
{
postId: 3,
nickname: '홍길동2',
title: '제목2 테스트입니다.',
createdAt: '2024-06-11T03:58:40.041Z',
updatedAt: '2024-06-11T03:58:40.041Z',
},
];
// 저장소(Repository)의 findAllPosts 메서드가 실행됐을 때, 반환되는 값 설정
mockPostsRepository.findAllPosts.mockReturnValue(samplePosts);
/* 실행 부분, 실제 서비스(Service)의 findAllPosts 메서드 실행 */
const allPosts = await postsService.findAllPosts();
/* 테스트(조건) 부분 */
// 서비스(Service)의 findAllPosts 메서드 반환값 allPosts와
// 저장소(Repository)의 findAllPosts 메서드의 반환값 samplePosts의 정렬과 같은지 검사
expect(allPosts).toEqual(
samplePosts.sort((a, b) => {
return b.createdAt - a.createdAt;
})
);
// 저장소(Repository)의 findAllPosts 메서드가 1번만 실행되는지 검사
expect(mockPostsRepository.findAllPosts).toHaveBeenCalledTimes(1);
});
test('deletePost Method By Success', async () => {
/* 설정 부분 */
// 저장소(Repository)의 deletePost 메서드의 반환값 (임시 결과값)
const samplePost = {
postId: 2,
nickname: '홍길동1',
password: '1234',
title: '제목1 테스트입니다.',
content: '테스트 코드용 내용입니다.',
createdAt: '2024-06-11T03:58:33.994Z',
updatedAt: '2024-06-11T03:58:33.994Z',
};
// 저장소(Repository)의 findPostById 메서드가 실행됐을 때, 반환되는 값 설정
mockPostsRepository.findPostById.mockReturnValue(samplePost);
/* 실행 부분, 실제 서비스(Service)의 deletePost 메서드 실행 */
const deletedPost = await postsService.deletePost(2, '1234');
/* 테스트(조건) 부분 */
// 저장소(Repository)의 findPostById 메서드가 1번만 실행되는지 검사
expect(mockPostsRepository.findPostById).toHaveBeenCalledTimes(1);
// 저장소(Repository)의 findPostById 메서드를 실행할 때,
// postId 데이터를 전달하는지 검사
expect(mockPostsRepository.findPostById).toHaveBeenCalledWith(
samplePost.postId
);
// 저장소(Repository)의 deletePost 메서드가 1번만 실행되는지 검사
expect(mockPostsRepository.deletePost).toHaveBeenCalledTimes(1);
// 저장소(Repository)의 deletePost 메서드를 실행할 때,
// postId, password 데이터를 전달하는지 검사
expect(mockPostsRepository.deletePost).toHaveBeenCalledWith(
samplePost.postId,
samplePost.password
);
// 서비스(Service)의 deletePost 메서드 반환값 deletedPost와 아래의 데이터 객체와 같은지 검사
expect(deletedPost).toEqual({
postId: samplePost.postId,
nickname: samplePost.nickname,
title: samplePost.title,
content: samplePost.content,
createdAt: samplePost.createdAt,
updatedAt: samplePost.updatedAt,
});
});
test('deletePost Method By Not Found Post Error', async () => {
/* 설정 부분 */
// 저장소(Repository)의 findPostById 메서드의 반환값 (임시 결과값)
// 고의로 에러를 만들기 위해 null 값을 사용 (post를 찾지 못했을 때의 에러)
const samplePost = null;
// 저장소(Repository)의 findPostById 메서드가 실행됐을 때, 반환되는 값 설정
mockPostsRepository.findPostById.mockReturnValue(samplePost);
// 무조건 에러가 발생하기 때문에 try catch문으로 에러 이후를 테스트
try {
/* 실행 부분, 실제 서비스(Service)의 deletePost 메서드 실행 */
await postsService.deletePost(123123, 'adsfasdf');
} catch (err) {
/* 테스트(조건) 부분 */
// 저장소(Repository)의 findPostById 메서드가 1번만 실행되는지 검사
expect(mockPostsRepository.findPostById).toHaveBeenCalledTimes(1);
// 저장소(Repository)의 findPostById 메서드를 실행할 때,
// postId 데이터를 전달하는지 검사
expect(mockPostsRepository.findPostById).toHaveBeenCalledWith(123123);
// 저장소(Repository)의 deletePost 메서드가 0번 실행되는지 검사
expect(mockPostsRepository.deletePost).toHaveBeenCalledTimes(0);
// err의 메세지가 '존재하지 않는 게시글입니다.'와 일치하는지 검사
expect(err.message).toEqual('존재하지 않는 게시글입니다.');
}
});
});
Controller는 API가 호출되었을 때 가장 처음으로 실행되는 계층
Controller는 클라이언트가 전달한 요청(Request)의 유효성 검사 및 데이터를 Service Layer로 전달하는 계층
Service Layer를 Mocking하여 독립적으로 테스트
// src/controllers/posts.controller.js
// 사용자의 입력을 받아서 Service로 넘기는 역할의 클래스
export class PostsController {
// Post 서비스 클래스를 컨트롤러 클래스의 멤버 변수로 할당
constructor(postsService) {
this.postsService = postsService;
}
// 게시물 목록 조회 메서드
getPosts = async (req, res, next) => {
try {
// 서비스 계층에 구현된 findAllPosts 로직을 실행합니다.
const posts = await this.postsService.findAllPosts();
return res.status(200).json({ data: posts });
} catch (err) {
next(err);
}
};
...
// 게시물 생성 메서드
createPost = async (req, res, next) => {
try {
const { nickname, password, title, content } = req.body;
if (!nickname || !password || !title || !content) {
throw new Error('InvalidParamsError');
}
// 서비스 계층에 구현된 createPost 로직을 실행합니다.
const createdPost = await this.postsService.createPost(
nickname,
password,
title,
content
);
return res.status(201).json({ data: createdPost });
} catch (err) {
next(err);
}
};
...
// __tests__/unit/controllers/posts.controller.unit.spec.js
import { expect, jest } from '@jest/globals';
import { PostsController } from '../../../src/controllers/posts.controller.js';
// posts.service.js 에서는 아래 5개의 Method만을 사용
const mockPostsService = {
findAllPosts: jest.fn(),
findPostById: jest.fn(),
createPost: jest.fn(),
updatePost: jest.fn(),
deletePost: jest.fn(),
};
// Request 요청 Mocking (req)
const mockRequest = {
body: jest.fn(),
};
// Response 요청 Mocking (res)
const mockResponse = {
status: jest.fn(),
json: jest.fn(),
};
// Next 요청 Mocking (next)
const mockNext = jest.fn();
// postsController의 Service를 Mock Service로 의존성을 주입
const postsController = new PostsController(mockPostsService);
describe('Posts Controller Unit Test', () => {
// 각 test가 실행되기 전에 실행
beforeEach(() => {
jest.resetAllMocks(); // 모든 Mock을 초기화
// mockResponse.status의 경우 메서드 체이닝으로 인해 반환값이 Response(자신: this)로 설정
mockResponse.status.mockReturnValue(mockResponse);
});
test('getPosts Method by Success', async () => {
/* 설정 부분 */
// 서비스(Service)의 findAllPosts 메서드의 반환값 (임시 결과값)
const samplePosts = [
{
postId: 2,
nickname: 'Nickname_2',
title: 'Title_2',
createdAt: new Date('07 October 2011 15:50 UTC'),
updatedAt: new Date('07 October 2011 15:50 UTC'),
},
{
postId: 1,
nickname: 'Nickname_1',
title: 'Title_1',
createdAt: new Date('06 October 2011 15:50 UTC'),
updatedAt: new Date('06 October 2011 15:50 UTC'),
},
];
// 서비스(Service)의 findAllPosts 메서드가 실행됐을 때, 반환되는 값 설정
mockPostsService.findAllPosts.mockReturnValue(samplePosts);
/* 실행 부분, 실제 컨트롤러(Controller)의 getPosts 메서드 실행 */
await postsController.getPosts(mockRequest, mockResponse, mockNext);
/* 테스트(조건) 부분 */
// 서비스(Service)의 findAllPosts 메서드가 1번만 실행되는지 검사
expect(mockPostsService.findAllPosts).toHaveBeenCalledTimes(1);
// Mocking된 응답(Response) 객체의 status 메서드가 1번만 실행되는지 검사
expect(mockResponse.status).toHaveBeenCalledTimes(1);
// Mocking된 응답(Response) 객체의 status 메서드를 실행할 때, 200 상태코드가 전달되는지 검사
expect(mockResponse.status).toHaveBeenCalledWith(200);
// Mocking된 응답(Response) 객체의 json 메서드가 1번만 실행되는지 검사
expect(mockResponse.json).toHaveBeenCalledTimes(1);
//Mocking된 응답(Response) 객체의 json 메서드를 실행할 때, samplePosts를 전달하는지 검사
expect(mockResponse.json).toHaveBeenCalledWith({
data: samplePosts,
});
});
test('createPost Method by Success', async () => {
/* 설정 부분 */
// 컨트롤러(Controller)의 createPost 메서드가 실행되기 위한 Body 입력 인자값 설정 (임시 입력값)
const createPostRequestBodyParams = {
nickname: 'Nickname_Success',
password: 'Password_Success',
title: 'Title_Success',
content: 'Content_Success',
};
// 요청(Request)의 body에 입력할 인자값 설정
mockRequest.body = createPostRequestBodyParams;
// 서비스(Service)의 createPost 메서드 반환값 데이터 형식 설정 (임시 결과값)
const createPostReturnValue = {
postId: 1,
...createPostRequestBodyParams,
createdAt: new Date().toString(),
updatedAt: new Date().toString(),
};
// 서비스(Service)의 createPost 메서드가 실행되었을 때의 반환값 설정
mockPostsService.createPost.mockReturnValue(createPostReturnValue);
/* 실행 부분, 실제 컨트롤러(Controller)의 createPost 메서드 실행 */
await postsController.createPost(mockRequest, mockResponse, mockNext);
/* 테스트(조건) 부분 */
// 서비스(Service)의 createPost 메서드가 1번만 실행되는지 검사
expect(mockPostsService.createPost).toHaveBeenCalledTimes(1);
// 서비스(Service)의 createPost 메서드를 실행할 때, req.body의 값들이 전달되는지 검사
expect(mockPostsService.createPost).toHaveBeenCalledWith(
createPostRequestBodyParams.nickname,
createPostRequestBodyParams.password,
createPostRequestBodyParams.title,
createPostRequestBodyParams.content
);
// Response status 검증
// Mocking된 응답(Response)의 status 메서드가 1번만 실행되는지 검사
expect(mockResponse.status).toHaveBeenCalledTimes(1);
// Mocking된 응답(Response)의 status 메서드를 실행할 때, 201 상태코드를 전달하는지 검사
expect(mockResponse.status).toHaveBeenCalledWith(201);
// Response json 검증
// Mocking된 응답(Response)의 json 메서드가 1번만 실행되는지 검사
expect(mockResponse.json).toHaveBeenCalledTimes(1);
// Mocking된 응답(Response)의 json 메서드를 실행할 때,
// data로 서비스(Service)의 createPost 메서드의 반환값이 전달되는지 검사
expect(mockResponse.json).toHaveBeenCalledWith({
data: createPostReturnValue,
});
});
test('createPost Method by Invalid Params Error', async () => {
/* 설정 부분 */
// 요청(Request)의 body에 입력할 인자값 설정 (에러 확인용)
mockRequest.body = {
nickname: 'Nickname_InvalidParamsError',
password: 'Password_InvalidParamsError',
};
/* 실행 부분, 실제 컨트롤러(Controller)의 createPost 메서드 실행 */
await postsController.createPost(mockRequest, mockResponse, mockNext);
/* 테스트(조건) 부분 */
// Mocking된 Next 메서드를 실행했을 때, 에러 메세지가 전달되는지 검사
expect(mockNext).toHaveBeenCalledWith(new Error('InvalidParamsError'));
});
});
오늘 강의 정리가 끝났기에 오늘부터 계속 개인과제 구현을 할 예정
우선 기존의 코드 일부분을 리팩토링 할 예정
그 다음에 Layered Architecture Pattern를 적용해서 코드 모듈화 진행
테스트 코드도 적용할 예정
Layered Architecture Pattern과 테스트 코드에 대한 내용을 한 곳에서 정리하기에 너무 많아서 나눠서 정리함
테스트 코드 부분은 생각보다 이해가 되지 않음
특히 단위 테스트 코드 예제 작성 시 코드를 어떤 흐름으로 작성하는 지 감이 오지 않음
일단은 코드마다 주석을 달아서 구조부터 이해해야겠음
그래도 주석을 달면서 해석하다보니 각 Jest 메서드들의 사용방법을 조금 알게 되었음
// package.json
{
...
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest"
},
...
}
basedir=$(dirname "$(echo | sed -e 's,\\,/,g')")
SyntaxError: missing ) after argument list
...
인터넷에 찾아보니 이 스크립트에서 발생한 문제는 셸 스크립트 내에서 $(dirname ...) 표현식에 의한 문제라고 나왔음
솔직히 무슨 말인지는 잘 모르겠지만 디렉터리 이름에서의 차이 때문에 생긴 것 같음
해결 방법은 두 가지가 있음
해결 방법 1) 셸 스크립트를 우회하는 방법
// package.json
{
...
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
...
}
# DevDependencies로 cross-env 를 설치
yarn add -D cross-env
// package.json
{
...
"type": "module",
"scripts": {
"test": "test": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest"
},
...
}