앞으로 어떻게 할 지
GET /boards, POST /board, … 이렇게 모든 테스트 코드를 다 짠 다음에 구현
GET /boards 테스트 코드 작성 후 구현 → 확인, POST /board 테스트 코드 작성 후 구현 → 확인, ,,,
⇒ 2번 방식 채택!
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from '../../src/auth/auth.controller';
import { AuthService } from '../../src/auth/auth.service';
describe('AuthController', () => {
let controller: AuthController;
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [AuthService],
}).compile();
controller = module.get<AuthController>(AuthController);
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('findAll', () => {
it('should be defined', () => {
expect(controller.findAll).toBeDefined();
});
it('should return an array of auth', async () => {
const result = ['test'];
jest.spyOn(service, 'findAll').mockImplementation((): any => result);
expect(await controller.findAll()).toBe(result);
});
});
describe('findOne', () => {
it('should be defined', () => {
expect(controller.findOne).toBeDefined();
});
it('should return an auth', async () => {
const result = 'test';
jest.spyOn(service, 'findOne').mockImplementation((): any => result);
expect(await controller.findOne('1')).toBe(result);
});
});
describe('create', () => {
it('should be defined', () => {
expect(controller.create).toBeDefined();
});
it('should return an auth', async () => {
const result = 'test';
jest.spyOn(service, 'create').mockImplementationOnce((): any => result);
expect(await controller.create({})).toMatchObject(result);
});
});
describe('update', () => {
it('should be defined', () => {
expect(controller.update).toBeDefined();
});
it('should return an auth', async () => {
const result = 'test';
jest.spyOn(service, 'update').mockImplementation((): any => result);
expect(await controller.update('1', {})).toBe(result);
});
});
describe('remove', () => {
it('should be defined', () => {
expect(controller.remove).toBeDefined();
});
});
});
Q. 이런 식으로 mocking하지 않은 채 repository까지 잘 동작하는지 테스트 코드를 작성할 필요가 있지 않을까요? 일반적으로 TDD를 어떻게 진행하는지 궁금합니다.
describe('create', () => {
it('should be defined', () => {
expect(controller.create).toBeDefined();
});
it('should return an auth (mock service)', async () => {
const result = 'test';
jest.spyOn(service, 'create').mockImplementationOnce((): any => result);
expect(await controller.create({})).toMatchObject(result);
});
it('should return an auth (real)', async () => {
// given
const user = 'test'; // TODO : 제대로된 User 인스턴스가 들어가야 함.
// when
const result: any = await controller.create(user);
// then
expect(result).toBeInstanceOf(User);
expect(result).toMatchObject(user);
});
it('should assign new id (real)', async () => {
// given
const user = 'test'; // TODO : 제대로된 User 인스턴스가 들어가야 함.
// when
const result: any = await controller.create(user);
expect(result).toHaveProperty('id');
expect(result.id).toBeInstanceOf(Number);
});
});
// board.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { BoardService } from './board.service';
import { Board } from './entities/board.entity';
describe('BoardService', () => {
let service: BoardService;
let repository: Repository<Board>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
BoardService,
{
provide: getRepositoryToken(Board),
useClass: Repository,
},
],
}).compile();
service = module.get<BoardService>(BoardService);
repository = module.get<Repository<Board>>(getRepositoryToken(Board));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('findAll', () => {
it('should be defined', () => {
expect(service.findAll).toBeDefined();
});
it('should return an array of boards', async () => {
const board = new Board();
board.id = 1;
board.title = 'test';
board.content = 'test';
board.image = null;
board.star_style = 'test';
board.star_position = 'POINT(0, 0, 0)';
board.author = 'test';
board.created_at = new Date();
board.modified_at = new Date();
const boards = [board];
jest.spyOn(repository, 'find').mockImplementation(async () => boards);
expect(await service.findAll()).toBe(boards);
});
});
});
Q. 여기서도 마찬가지로 mocking 없이 repository 테스트를 해야 하지 않을까요? board.repository.spec.ts를 따로 만들어서 이 부분을 테스트 한다면 어떻게 해야 할지.. 감이 잡히지 않습니다.
describe('findAll', () => {
it('should be defined', () => {
expect(service.findAll).toBeDefined();
});
it('should return an array of boards (mock repository)', async () => {
const board = new Board();
board.id = 1;
board.title = 'test';
board.content = 'test';
board.image = null;
board.star_style = 'test';
board.star_position = 'POINT(0, 0, 0)';
board.author = 'test';
board.created_at = new Date();
board.modified_at = new Date();
const boards = [board];
jest.spyOn(repository, 'find').mockImplementation(async () => boards);
expect(await service.findAll()).toBe(boards);
});
it('should return an array of boards (real)', async () => {
const board = new Board();
board.id = 1;
board.title = 'test';
board.content = 'test';
board.image = null;
board.star_style = 'test';
board.star_position = 'POINT(0, 0, 0)';
board.author = 'test';
board.created_at = new Date();
board.modified_at = new Date();
const boards = [board];
await service.create(board); // 빈 repository에 실제로 넣어서
expect(await service.findAll()).toBe(boards);
});
});
요약하면 게시글의 좋아요 기능에서 발생할 수 있는 동시성 이슈와, 이를 해결할 수 있는 트랜잭션과 MySQL의 락(공유 락 lock in share mode
와 배타 락 for update
)을 통해 이를 해결하는 과정에 대한 포스트입니다.
저희도 좋아요와 게시글 수정 기능이 있고, 이러한 update 과정에서 발생할 수 있는 동시성 이슈를, 트랜잭션과 락을 통해 해결하는 과정을 기록으로 남겨보려고 합니다.
Q. 추가적으로, 이 과정에서 동시성 이슈를 실제로 유발하는 테스트를 하고, 해결하는 과정으로 구현을 진행하고 싶은데 방법을 잘 모르겠습니다. 이를 jest로 테스트할 수 있는 방법에 대해서 알려주실 수 있을까요?
동시성 제어 딥다이브의 의도에 관해
shared lock
걸거나 큐
처리TDD 관련 Repository를 써야 되나에 대해
TDD와 일반적인 Test코드는 다름!
TDD는 Red Green Refactor 등과 같은 방식으로 진행됨.
Mocking을 하거나 TestDB를 넣어서 실제로 테스트하던지 이건 사람마다 다름
BDD(behavior driven dev)라고 해서 상황을 만들고 거기에 대한 테스트코드를 작성하기도 함
TDD의 철학은 모든 요구사항을 Test로 적는다는 느낌.
멘토님의 TDD 예시 (커밋로그 보세요)
GitHub - linusdamyo/wandookong-study
⭐️ 글 쓰기 기능(POST /articles
)에 대한 TDD 예시(테스트 코드) ⭐️
Synchronize: true
로 하면, Memory-DB(sqlite 등)를 하면 잘 되더라.test
용 dev
용 설정해놓고 이걸 Database를 다르게 써서 하면됨.Repository를 따로 빼는가? 아니면 그냥 Service에 때려넣는가?
공식문서에서는 Service에 비즈니스 로직을 다 넣고, Repository에 지정된 메소드만 이용하는 방식을 추천하지만
멘토님께서는 준섭님 방식(=보통의 Service처럼 Provider에 넣어서 따로 Custom Repository처럼 계층을 나누는 방식)과 유사하게 하심
(여기서 포인트는 getRepository()로 그때그때 repository 객체를 불러오는거)
Repository는 이렇게
Service는 이렇게