앞선 mocking방식의 unit 테스트가 실제 db연동이 잘 되는지 확인하기 힘들고, TDD를 적용하기가 상당히 까다롭다는 판단 + 멘토님의 의견으로
unit 테스트 대신 e2e(end-to-end) 테스트
를 작성하여 TDD를 하도록 전략을 바꿔 보았습니다.
TDD 방식은 흔히 사용되는 RED-GREEN-REFACTOR 방식을 학습하여 적용해 보았습니다.
간단히 설명드리자면 각 기능별로 다음의 순서로 개발을 진행하는 것입니다.
그럼 바로 가시죠
다음 이슈를 분석해 요구사항을 구체화하고 e2e 테스트로 만들자
학습메모 4를 참고하여, Red-Green-Refactor 방식을 준용해 보았다.
board가 단순 CRUD에 더 가까우므로 먼저 해보기로 했다.
test/board
에 board.e2e-spec.ts
파일 생성
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../src/app.module';
describe('BoardController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
describe('/board', () => {
// #39 [06-02] 서버는 사용자의 글 데이터를 전송한다.
it.todo('GET /board/:id');
// (추가 필요) 서버는 사용자의 글 목록을 전송한다.
it.todo('GET /board');
// #45 [06-08] 서버는 좋아요 / 좋아요 취소 요청을 받아 데이터베이스의 데이터를 수정한다.
it.todo('PUT /board/:id/like');
it.todo('PUT /board/:id/unlike');
// #60 [08-06] 서버는 전송 받은 데이터를 데이터베이스에 저장한다.
it.todo('POST /board');
// #65 [09-03] 서버는 검색된 사용자의 글 데이터를 전송한다.
it.todo('GET /board/by-author');
// (추가 필요) 서버는 사용자의 요청에 따라 글을 수정한다.
it.todo('PUT /board/:id');
// (추가 필요) 서버는 사용자의 요청에 따라 글을 삭제한다.
it.todo('DELETE /board/:id');
});
});
todo로 우선 요구사항 및 API 명세를 등록했다.
POST /board
에 대해 테스트 코드를 작성해보자.
// #60 [08-06] 서버는 전송 받은 데이터를 데이터베이스에 저장한다.
it('POST /board', () => {
const board = {
title: 'test',
content: 'test',
author: 'test',
};
return request(app.getHttpServer())
.post('/board')
.send(board)
.expect(201, {
id: 1,
...board,
});
});
yarn workspace server test '.../board.e2e-spec.ts'
재밌는게 Nest에서 201 응답은 이미 처리해줘버림ㅋ 이제 여기를 통과하게 Entity랑 DTO 만들고 Controller, Service 코드를 수정해보자.
// board.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Board extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 255, nullable: false })
title: string;
@Column({ type: 'text', nullable: true })
content: string;
@Column({ type: 'varchar', length: 50, nullable: false })
author: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
created_at: Date;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updated_at: Date;
}
// create-board.dto.ts
export class CreateBoardDto {
title: string;
content: string;
author: string;
}
이제 컨트롤러, 서비스를 수정해주자. DB까지 연동!
// board.controller.ts
@Controller('board')
export class BoardController {
constructor(private readonly boardService: BoardService) {}
@Post()
create(@Body() createBoardDto: CreateBoardDto): Promise<Board> {
return this.boardService.create(createBoardDto);
}
...
}
// board.service.ts
...
@Injectable()
export class BoardService {
constructor(
@InjectRepository(Board)
private boardRepository: Repository<Board>,
) {}
async create(createBoardDto: CreateBoardDto): Promise<Board> {
const { title, content, author } = createBoardDto;
const board = await this.boardRepository.create({
title,
content,
author,
});
const created: Board = await this.boardRepository.save(board);
return created;
}
...
}
repository도 주입해주고, 생성한 Board 인스턴스를 리턴하도록 해 id값을 얻을 수 있게 해줬다.
여기까지 테스트하니 id를 1으로 고정시켜놓은 것도 그렇고, created_at 등 추가된 컬럼과
앞으로 추가될 컬럼까지 고려해서 수정하지 않아도 되는 테스트를 만들고 싶었다.
// #60 [08-06] 서버는 전송 받은 데이터를 데이터베이스에 저장한다.
it('POST /board', async () => {
const board = {
title: 'test',
content: 'test',
author: 'test',
};
const response = await request(app.getHttpServer())
.post('/board')
.send(board)
.expect(201);
expect(response).toHaveProperty('body');
expect((response as any).body).toMatchObject(board);
expect((response as any).body).toHaveProperty('id');
expect(typeof response.body.id).toBe('number');
});
그래서 위와 같이 테스트코드를 수정함.
아름답게 통과된다! 리팩토링은 따로 필요없을 것 같아 생략.
이후부터는 방법론 명칭대로 다음과 같이 간략히 기재하겠음.
// board.e2e-spec.ts
// #39 [06-02] 서버는 사용자의 글 데이터를 전송한다.
it('GET /board/:id', async () => {
const response = await request(app.getHttpServer())
.get('/board/1')
.expect(200);
expect(response).toHaveProperty('body');
expect((response as any).body).toHaveProperty('id');
expect(response.body.id).toBe(1);
expect((response as any).body).toHaveProperty('title');
expect((response as any).body).toHaveProperty('content');
expect((response as any).body).toHaveProperty('author');
expect((response as any).body).toHaveProperty('created_at');
expect((response as any).body).toHaveProperty('updated_at');
});
실패하는 테스트 코드를 작성해준다.
id가 일치해야 하며, 다음 기대되는 속성들을 받아올 수 있어야 함:
id, title, content, author, created_at, updated_at
// board.service.ts
async findOne(id: number) {
const found: Board = await this.boardRepository.findOneBy({ id });
return found;
}
서비스 메소드인 findOne()을 위와 같이 수정해주면 된다.
예외처리와 적절한 함수이름으로의 변경, 타입 명시 등을 추가로 처리해줬다.
참고로 입력값에 대한 유효성 검증 등은 기본기능 구현 후 추가할 예정.
// board.controller.ts
@Get(':id')
getBoardById(@Param('id') id: string): Promise<Board> {
return this.boardService.getBoardById(+id);
}
// board.service.ts
async getBoardById(id: number): Promise<Board> {
const found: Board = await this.boardRepository.findOneBy({ id });
if (!found) {
throw new NotFoundException(`Not found board with id: ${id}`);
}
return found;
}
통과는 마찬가지로 잘 된다.
Not Found Exception 처리도 확인 (이것도 추후 테스트 추가해주면 좋을 듯 하다)