-
Notifications
You must be signed in to change notification settings - Fork 2
[재하] 1209(토) 개발기록
- 서비스 유닛 테스트, mocking
- e2e 테스트, 파일 첨부
- 결과 화면
비즈니스 로직 전체에 대한 검증은 e2e 테스트에서 커버해주기 때문에, 유닛 테스트에서는 함수 모킹을 통해 서비스 메소드의 에러처리가 잘 수행되는지를 꼼꼼하게 확인해줬다.
board 모듈에는 transaction 제어, 복수개의 서비스 파일, 그리고 TypeORM(MySQL) Repository 뿐만 아니라 Mongoose(MongoDB) Model도 의존성에 포함되어 이것들을 모킹하는 방법을 찾는 게 참 어려웠다(자료가 많이 없다). 많은 시행착오 끝에 성공!
방금 언급한 대상들을 유닛 테스트를 위해 모킹하는 방법을 간단히 정리해봤다.
자료찾기 정말 힘들었다..찡찡
export class BoardService {
constructor(
private readonly dataSource: DataSource,
) {}
...
async updateBoard(
id: number,
updateBoardDto: UpdateBoardDto,
userData: UserDataDto,
files: Express.Multer.File[],
): Promise<Board> {
let updatedBoard: Board;
await this.dataSource.transaction(async (manager: EntityManager) => {
const board: Board = await manager.findOneBy(Board, { id });
if (!board) {
throw new NotFoundException('board not found');
}
// 게시글 작성자와 수정 요청자가 다른 경우
if (board.user.id !== userData.userId) {
throw new BadRequestException('not your post');
}
...
});
return updatedBoard;
}
}
mocking할 transaction 메소드는 위와 같이 사용된다. DataSource 객체를 constructor에 포함하고, 콜백 함수를 transaction에 파라미터로 넘긴다.
콜백 함수의 파라미터에는 EntityManager라는 객체가 들어가는데, 이 친구의 메소드로 기존의 TypeORM 쿼리 메소드인 findOneBy, create, remove 등을 사용하는 식이다.
let boardService: BoardService;
let dataSource: jest.Mocked<DataSource>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
BoardService,
...
{
provide: DataSource,
useValue: {
transaction: jest.fn(),
manager: {
findOneBy: jest.fn(),
delete: jest.fn(),
},
},
},
...
],
}).compile();
boardService = module.get<BoardService>(BoardService);
...
dataSource = module.get(DataSource);
});
이 부분을 mocking하려면 먼저 dataSource의 타입을 jest.Mocked<DataSource>
로 지정해주고,
beforeEach문을 활용해 위와 같이 transaction과 manager을 미리 정의해줘야 한다.
describe('updateBoard', () => {
it('should return NotFoundException with not existed board', async () => {
jest
.spyOn(dataSource, 'transaction')
.mockImplementation(async (callback: any) => {
await callback(dataSource.manager);
});
jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue(undefined);
await expect(
boardService.updateBoard(1, {} as any, {} as any, []),
).rejects.toThrow('board not found');
await expect(
boardService.updateBoard(1, {} as any, {} as any, []),
).rejects.toThrow(NotFoundException);
});
});
그 후 dataSource의 transaction 메소드에 대한 mocking을 jest.spyOn
의 mockImplementation
메소드로 정의해준다.
callback에는 dataSource.manager
를 전달해주면 되고, manager의 메소드를 정의하려면 위 코드와 같이
dataSource.manager에 대해 jest.spyOn
으로 mocking해주면 된다. 여기서는 mockResolvedValue
로 리턴값을 정의해줬다.
let boardService: BoardService;
let fileService: FileService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BoardService, FileService],
}).compile();
boardService = module.get<BoardService>(BoardService);
fileService = module.get<FileService>(FileService);
});
서비스 객체는 providers에 객체 타입을 넣어주고, module.get
으로 그 타입을 그대로 파라미터로 넣어주면 된다.
메소드 mocking은 클래스에 정의된 메소드면 에러 없이 잘 된다.
let userRepository: Repository<User>;
let boardRepository: Repository<Board>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getRepositoryToken(User),
useClass: Repository,
},
{
provide: getRepositoryToken(Board),
useClass: Repository,
},
],
}).compile();
userRepository = module.get<Repository<User>>(getRepositoryToken(User));
boardRepository = module.get<Repository<Board>>(getRepositoryToken(Board));
});
TypeORM 리포지토리는 위와 같이 getRepositoryToken()
이라는 함수를 활용해 프로바이더를 정의해줘야 한다.
Object에 provide속성으로 getRepositoryToken([entity 타입])
, useClass속성으로 Repository
를 넣어준다.
그러면 TypeORM에 정의된 메소드들에 대해 Mocking이 가능하다.
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(undefined);
요런식으로
let starModel: Model<Star>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getModelToken(Star.name),
useValue: Model,
},
],
}).compile();
starModel = module.get<Model<Star>>(getModelToken(Star.name));
});
Mongoose모델은 비슷한데 좀 다르다(이것도 자료찾기가 참 힘들었다ㅎ)
메소드 모킹은 똑같이 하면 된다.
import { Test, TestingModule } from '@nestjs/testing';
import { BoardService } from '../../src/board/board.service';
import { DataSource, Repository } from 'typeorm';
import { User } from '../../src/auth/entities/user.entity';
import { Board } from '../../src/board/entities/board.entity';
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
import { Model } from 'mongoose';
import { Star } from '../../src/star/schemas/star.schema';
import { getModelToken } from '@nestjs/mongoose';
import {
BadRequestException,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { FileService } from '../../src/board/file.service';
import { UserDataDto } from 'src/auth/dto/user-data.dto';
describe('BoardService', () => {
let boardService: BoardService;
let fileService: FileService;
let dataSource: jest.Mocked<DataSource>;
let userRepository: Repository<User>;
let boardRepository: Repository<Board>;
let starModel: Model<Star>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
BoardService,
FileService,
{
provide: DataSource,
useValue: {
transaction: jest.fn(),
manager: {
findOneBy: jest.fn(),
delete: jest.fn(),
},
},
},
{
provide: getRepositoryToken(User),
useClass: Repository,
},
{
provide: getRepositoryToken(Board),
useClass: Repository,
},
{
provide: getModelToken(Star.name),
useValue: Model,
},
],
}).compile();
boardService = module.get<BoardService>(BoardService);
fileService = module.get<FileService>(FileService);
userRepository = module.get<Repository<User>>(getRepositoryToken(User));
boardRepository = module.get<Repository<Board>>(getRepositoryToken(Board));
starModel = module.get<Model<Star>>(getModelToken(Star.name));
dataSource = module.get(DataSource);
});
it('should be defined', () => {
expect(boardService).toBeDefined();
});
describe('getIsLiked', () => {
it('should return NotFoundException with not existed board', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(undefined);
await expect(boardService.getIsLiked(1, userData)).rejects.toThrow(
'board not found',
);
await expect(boardService.getIsLiked(1, userData)).rejects.toThrow(
NotFoundException,
);
});
it('should return true if already liked', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
const boardData: Board = new Board();
boardData.likes = [{ id: 1 } as any];
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData);
await expect(boardService.getIsLiked(1, userData)).resolves.toBe(true);
});
it('should return false if not liked', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
const boardData: Board = new Board();
boardData.likes = [];
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData);
await expect(boardService.getIsLiked(1, userData)).resolves.toBe(false);
});
});
describe('patchLike', () => {
it('should return NotFoundException with not existed board', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(undefined);
await expect(boardService.patchLike(1, userData)).rejects.toThrow(
'board not found',
);
await expect(boardService.patchLike(1, userData)).rejects.toThrow(
NotFoundException,
);
});
it('should return BadRequestException with already liked', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
const boardData: Board = new Board();
boardData.likes = [{ id: 1 } as any];
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData);
await expect(boardService.patchLike(1, userData)).rejects.toThrow(
'already liked',
);
await expect(boardService.patchLike(1, userData)).rejects.toThrow(
BadRequestException,
);
});
it('should return NotFoundException with not existed user', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
jest
.spyOn(boardRepository, 'findOneBy')
.mockResolvedValue({ likes: [] } as any);
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(undefined);
await expect(boardService.patchLike(1, userData)).rejects.toThrow(
'user not found',
);
await expect(boardService.patchLike(1, userData)).rejects.toThrow(
NotFoundException,
);
});
});
describe('patchUnlike', () => {
it('should return NotFoundException with not existed board', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(undefined);
await expect(boardService.patchUnlike(1, userData)).rejects.toThrow(
'board not found',
);
await expect(boardService.patchUnlike(1, userData)).rejects.toThrow(
NotFoundException,
);
});
it('should return BadRequestException with not liked', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
const boardData: Board = new Board();
boardData.likes = [];
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData);
await expect(boardService.patchUnlike(1, userData)).rejects.toThrow(
'not liked',
);
await expect(boardService.patchUnlike(1, userData)).rejects.toThrow(
BadRequestException,
);
});
it('should return NotFoundException with not existed user', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
const boardData: Board = new Board();
boardData.likes = [{ id: 1 } as any];
jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData);
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(undefined);
await expect(boardService.patchUnlike(1, userData)).rejects.toThrow(
'user not found',
);
await expect(boardService.patchUnlike(1, userData)).rejects.toThrow(
NotFoundException,
);
});
});
describe('deleteBoard', () => {
it('should return NotFoundException with not existed board', async () => {
jest
.spyOn(dataSource, 'transaction')
.mockImplementation(async (callback: any) => {
await callback(dataSource.manager);
});
jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue(undefined);
await expect(boardService.deleteBoard(1, {} as any)).rejects.toThrow(
'board not found',
);
await expect(boardService.deleteBoard(1, {} as any)).rejects.toThrow(
NotFoundException,
);
});
it('should return BadRequestException with not your board', async () => {
jest
.spyOn(dataSource, 'transaction')
.mockImplementation(async (callback: any) => {
await callback(dataSource.manager);
});
jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue({
user: { id: 2, username: 'username', nickname: 'nickname' },
} as any);
await expect(boardService.deleteBoard(1, {} as any)).rejects.toThrow(
'not your post',
);
await expect(boardService.deleteBoard(1, {} as any)).rejects.toThrow(
BadRequestException,
);
});
it('should delte images', async () => {
const userData: UserDataDto = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
jest
.spyOn(dataSource, 'transaction')
.mockImplementation(async (callback: any) => {
await callback(dataSource.manager);
});
jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue({
user: { id: 1, username: 'username', nickname: 'nickname' },
images: [{ id: 1, key: 'key' }],
} as any);
jest.spyOn(dataSource.manager, 'delete').mockResolvedValue({
affected: 1,
} as any);
jest.spyOn(fileService, 'deleteFile').mockResolvedValue(undefined);
expect(await boardService.deleteBoard(1, userData)).toMatchObject({
affected: 1,
} as any);
});
});
describe('updateBoard', () => {
it('should return NotFoundException with not existed board', async () => {
jest
.spyOn(dataSource, 'transaction')
.mockImplementation(async (callback: any) => {
await callback(dataSource.manager);
});
jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue(undefined);
await expect(
boardService.updateBoard(1, {} as any, {} as any, []),
).rejects.toThrow('board not found');
await expect(
boardService.updateBoard(1, {} as any, {} as any, []),
).rejects.toThrow(NotFoundException);
});
it('should return BadRequestException with not your board', async () => {
const userData = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
jest
.spyOn(dataSource, 'transaction')
.mockImplementation(async (callback: any) => {
await callback(dataSource.manager);
});
jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue({
user: { id: 2, username: 'username', nickname: 'nickname' },
} as any);
await expect(
boardService.updateBoard(1, {} as any, userData, []),
).rejects.toThrow('not your post');
await expect(
boardService.updateBoard(1, {} as any, userData, []),
).rejects.toThrow(BadRequestException);
});
it('sould throw BadRequestException when request to update star', async () => {
const userData = {
userId: 1,
nickname: 'nickname',
username: 'username',
status: 'public',
};
const board = new Board();
board.user = { id: 1, username: 'username', nickname: 'nickname' } as any;
board.star = 'star_id';
jest
.spyOn(dataSource, 'transaction')
.mockImplementation(async (callback: any) => {
await callback(dataSource.manager);
});
jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue(board);
await expect(
boardService.updateBoard(1, { star: { a: 'b' } } as any, userData, []),
).rejects.toThrow('cannot update star');
await expect(
boardService.updateBoard(1, { star: { a: 'b' } } as any, userData, []),
).rejects.toThrow(BadRequestException);
});
});
afterEach(() => {
jest.clearAllMocks();
});
});
대충 이런 흐름
jest supertest환경에서 파일 첨부때문에 폼데이터로 요청을 보내야 하는데, 인터넷의 자료들 중에 잘 되는 게 있고 안되는 게 있어서, 현재기준 최신버전에서 잘 동작하는 방법으로 정리해 보았다.
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.use(cookieParser());
await app.init();
// 유저 만들고 로그인 후 accessToken 받아오기
const randomeBytes = Math.random().toString(36).slice(2, 10);
const newUser = {
username: randomeBytes,
nickname: randomeBytes,
password: randomeBytes,
};
await request(app.getHttpServer()).post('/auth/signup').send(newUser);
newUser.nickname = undefined;
const signInResponse = await request(app.getHttpServer())
.post('/auth/signin')
.send(newUser);
signInResponse.headers['set-cookie'].forEach((cookie: string) => {
if (cookie.includes('accessToken')) {
accessToken = cookie.split(';')[0].split('=')[1];
}
});
});
페어님이 연구해주신 부분<3
beforeEach 문을 활용해 일반적인 e2e를 위한 createTestingModule()
과정을 마치고,
JWT + 쿠키/세션 방식을 사용하는 우리 프로젝트에 맞게 supertest로 signup, signin을 해서 accessToken을 받아온다.
accessToken을 전역으로 받아오면, 이제 모든 supertest 요청에 cookie헤더를 달아줘야 하는데, 방법은
await request(app.getHttpServer())
.patch(`/post/${createdBoard.id}/unlike`)
.set('Cookie', [`accessToken=${accessToken}`])
.expect(200);
이런식으로, .set()
메소드로 Request Header를 추가시켜주면 된다.
it('PATCH /post/:id with images', async () => {
const board = {
title: 'test',
content: 'test',
star: '{}',
};
const createdBoard = (
await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.send(board)
).body;
expect(createdBoard).toHaveProperty('id');
const id = createdBoard.id;
const toUpdate: UpdateBoardDto = {
title: 'updated',
content: 'updated',
};
const updated = await request(app.getHttpServer())
.patch(`/post/${id}`)
.set('Cookie', [`accessToken=${accessToken}`])
.set('Content-Type', 'multipart/form-data')
.field('title', toUpdate.title)
.field('content', toUpdate.content)
.attach('file', Buffer.from(sampleImageBase64, 'base64'), {
filename: 'test_image_updated1.jpg',
contentType: 'image/jpg',
})
.attach('file', Buffer.from(sampleImageBase64, 'base64'), {
filename: 'test_image_updated2.jpg',
contentType: 'image/jpg',
})
.expect(200);
expect(updated).toHaveProperty('body');
const updatedBoard = updated.body;
expect(updatedBoard).toHaveProperty('id');
expect(updatedBoard.id).toBe(id);
expect(updatedBoard).toHaveProperty('title');
expect(updatedBoard.title).toBe(toUpdate.title);
expect(updatedBoard).toHaveProperty('content');
expect(updatedBoard.content).toBe(encryptAes(toUpdate.content));
expect(updatedBoard).toHaveProperty('images');
expect(Array.isArray(updatedBoard.images)).toBe(true);
});
메인 파트를 다시 추려보자면,
const updated = await request(app.getHttpServer())
.patch(`/post/${id}`)
.set('Cookie', [`accessToken=${accessToken}`])
.set('Content-Type', 'multipart/form-data')
.field('title', toUpdate.title)
.field('content', toUpdate.content)
.attach('file', Buffer.from(sampleImageBase64, 'base64'), {
filename: 'test_image_updated1.jpg',
contentType: 'image/jpg',
})
.attach('file', Buffer.from(sampleImageBase64, 'base64'), {
filename: 'test_image_updated2.jpg',
contentType: 'image/jpg',
})
.expect(200);
이렇게 .set('Content-Type', 'multipart/form-data')
으로 Content-Type을 multipart/formdata로 지정해주는 게 첫번째 포인트고,
이후 field()
메소드로 각 필드를 체인으로 하나하나 추가시켜 주면 된다.
파일 첨부를 위해서는 attach()
메소드를 사용하면 되는데, 이게 파라미터 타입과 사용 방식이 인터넷 자료마다 제각각인데,
안먹히는 경우가 좀 많았다. fs.readStram으로 하라는데도 있고, 그냥 경로만 쓰라는 데도 있고.. new File()로 하라는 데도 있고
// test image making
const fs = require('fs');
const buf = fs.readFileSync('./test_image.jpg');
const base64 = buf.toString('base64');
console.log(base64);
아무튼 이거저거 해도 잘 안되길래 이미지 파일을 직접 버퍼로 만들어서 base64로 인코딩해서 스트링을 채취(?)했음.
export const sampleImageBase64 =
'/9j/4AAQSkZJRgABAQAAkACQAAD/4QCeRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAACQAAAAAQAAAJAAAAABAAOShgAHAAAAEgAAAISgAgAEAAAAAQAAAAqgAwAEAAAAAQAAAAoAAAAAQVNDSUkAAABTY3JlZW5zaG90/+EJIWh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIi8+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/+IP0ElDQ19QUk9GSUxFAAEBAAAPwGFwcGwCEAAAbW50clJHQiBYWVogB+cAAgAUABAAOwA5YWNzcEFQUEwAAAAAQVBQTAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARZGVzYwAAAVAAAABiZHNjbQAAAbQAAAScY3BydAAABlAAAAAjd3RwdAAABnQAAAAUclhZWgAABogAAAAUZ1hZWgAABpwAAAAUYlhZWgAABrAAAAAUclRSQwAABsQAAAgMYWFyZwAADtAAAAAgdmNndAAADvAAAAAwbmRpbgAADyAAAAA+bW1vZAAAD2AAAAAodmNncAAAD4gAAAA4YlRSQwAABsQAAAgMZ1RSQwAABsQAAAgMYWFiZwAADtAAAAAgYWFnZwAADtAAAAAgZGVzYwAAAAAAAAAIRGlzcGxheQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAAAAAAJgAAAAxockhSAAAAFAAAAdhrb0tSAAAADAAAAexuYk5PAAAAEgAAAfhpZAAAAAAAEgAAAgpodUhVAAAAFAAAAhxjc0NaAAAAFgAAAjBkYURLAAAAHAAAAkZubE5MAAAAFgAAAmJmaUZJAAAAEAAAAnhpdElUAAAAGAAAAohlc0VTAAAAFgAAAqByb1JPAAAAEgAAArZmckNBAAAAFgAAAshhcgAAAAAAFAAAAt51a1VBAAAAHAAAAvJoZUlMAAAAFgAAAw56aFRXAAAACgAAAyR2aVZOAAAADgAAAy5za1NLAAAAFgAAAzx6aENOAAAACgAAAyRydVJVAAAAJAAAA1JlbkdCAAAAFAAAA3ZmckZSAAAAFgAAA4ptcwAAAAAAEgAAA6BoaUlOAAAAEgAAA7J0aFRIAAAADAAAA8RjYUVTAAAAGAAAA9BlbkFVAAAAFAAAA3Zlc1hMAAAAEgAAArZkZURFAAAAEAAAA+hlblVTAAAAEgAAA/hwdEJSAAAAGAAABApwbFBMAAAAEgAABCJlbEdSAAAAIgAABDRzdlNFAAAAEAAABFZ0clRSAAAAFAAABGZwdFBUAAAAFgAABHpqYUpQAAAADAAABJAATABDAEQAIAB1ACAAYgBvAGoAac7st+wAIABMAEMARABGAGEAcgBnAGUALQBMAEMARABMAEMARAAgAFcAYQByAG4AYQBTAHoA7QBuAGUAcwAgAEwAQwBEAEIAYQByAGUAdgBuAP0AIABMAEMARABMAEMARAAtAGYAYQByAHYAZQBzAGsA5gByAG0ASwBsAGUAdQByAGUAbgAtAEwAQwBEAFYA5AByAGkALQBMAEMARABMAEMARAAgAGEAIABjAG8AbABvAHIAaQBMAEMARAAgAGEAIABjAG8AbABvAHIATABDAEQAIABjAG8AbABvAHIAQQBDAEwAIABjAG8AdQBsAGUAdQByIA8ATABDAEQAIAZFBkQGSAZGBikEGgQ+BDsETAQ+BEAEPgQyBDgEOQAgAEwAQwBEIA8ATABDAEQAIAXmBdEF4gXVBeAF2V9pgnIATABDAEQATABDAEQAIABNAOAAdQBGAGEAcgBlAGIAbgD9ACAATABDAEQEJgQyBDUEQgQ9BD4EOQAgBBYEGgAtBDQEOARBBD8EOwQ1BDkAQwBvAGwAbwB1AHIAIABMAEMARABMAEMARAAgAGMAbwB1AGwAZQB1AHIAVwBhAHIAbgBhACAATABDAEQJMAkCCRcJQAkoACAATABDAEQATABDAEQAIA4qDjUATABDAEQAIABlAG4AIABjAG8AbABvAHIARgBhAHIAYgAtAEwAQwBEAEMAbwBsAG8AcgAgAEwAQwBEAEwAQwBEACAAQwBvAGwAbwByAGkAZABvAEsAbwBsAG8AcgAgAEwAQwBEA4gDswPHA8EDyQO8A7cAIAO/A7gDzAO9A7cAIABMAEMARABGAOQAcgBnAC0ATABDAEQAUgBlAG4AawBsAGkAIABMAEMARABMAEMARAAgAGEAIABjAG8AcgBlAHMwqzDpMPwATABDAER0ZXh0AAAAAENvcHlyaWdodCBBcHBsZSBJbmMuLCAyMDIzAABYWVogAAAAAAAA81EAAQAAAAEWzFhZWiAAAAAAAACD3wAAPb////+7WFlaIAAAAAAAAEq/AACxNwAACrlYWVogAAAAAAAAKDgAABELAADIuWN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANgA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCjAKgArQCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEB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';
그리고 이렇게 ㅋ 때려박아줬다. 크기 최대한 작게했는데 그래도 크다.. 하지만 잘됨
Buffer.from(sampleImageBase64, 'base64');
요러케 base64 디코딩해서 버퍼로 가져오면됨
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../src/app.module';
import { UpdateBoardDto } from '../../src/board/dto/update-board.dto';
import { CreateBoardDto } from '../../src/board/dto/create-board.dto';
import * as cookieParser from 'cookie-parser';
import { encryptAes } from '../../src/util/aes.util';
import { sampleImageBase64 } from './sample-image';
describe('BoardController (/board, e2e)', () => {
let app: INestApplication;
let accessToken: string;
let post_id: number;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.use(cookieParser());
await app.init();
// 유저 만들고 로그인 후 accessToken 받아오기
const randomeBytes = Math.random().toString(36).slice(2, 10);
const newUser = {
username: randomeBytes,
nickname: randomeBytes,
password: randomeBytes,
};
await request(app.getHttpServer()).post('/auth/signup').send(newUser);
newUser.nickname = undefined;
const signInResponse = await request(app.getHttpServer())
.post('/auth/signin')
.send(newUser);
signInResponse.headers['set-cookie'].forEach((cookie: string) => {
if (cookie.includes('accessToken')) {
accessToken = cookie.split(';')[0].split('=')[1];
}
});
// 별글도 하나 생성 후 수행
const board = {
title: 'test',
content: 'test',
star: '{}',
};
const postedBoard = await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.set('Content-Type', 'multipart/form-data')
.field('title', board.title)
.field('content', board.content)
.field('star', board.star)
.attach('file', Buffer.from(sampleImageBase64, 'base64'), {
filename: 'test_image.jpg',
contentType: 'image/jpg',
});
post_id = postedBoard.body.id;
});
// #60 [08-06] 서버는 전송 받은 데이터를 데이터베이스에 저장한다.
it('POST /post', async () => {
const board = {
title: 'test',
content: 'test',
star: '{}',
};
const response = await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.send(board)
.expect(201);
expect(response).toHaveProperty('body');
const { body } = response;
expect(body).toHaveProperty('id');
expect(typeof body.id).toBe('number');
expect(body).toHaveProperty('title');
expect(body.title).toBe(board.title);
expect(body).toHaveProperty('content');
expect(body.content).toBe(encryptAes(board.content)); // 암호화되었는지 확인
expect(body).toHaveProperty('star');
expect(typeof body.star).toBe('string');
});
it('POST /post with images', async () => {
const board = {
title: 'test',
content: 'test',
star: '{}',
};
const response = await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.set('Content-Type', 'multipart/form-data')
.field('title', board.title)
.field('content', board.content)
.field('star', board.star)
.attach('file', Buffer.from(sampleImageBase64, 'base64'), {
filename: 'test_image.jpg',
contentType: 'image/jpg',
})
.expect(201);
expect(response).toHaveProperty('body');
const { body } = response;
expect(body).toHaveProperty('id');
expect(typeof body.id).toBe('number');
expect(body).toHaveProperty('title');
expect(body.title).toBe(board.title);
expect(body).toHaveProperty('content');
expect(body.content).toBe(encryptAes(board.content)); // 암호화되었는지 확인
expect(body).toHaveProperty('star');
expect(typeof body.star).toBe('string');
expect(body).toHaveProperty('images');
expect(Array.isArray(body.images)).toBe(true);
});
// #39 [06-02] 서버는 사용자의 글 데이터를 전송한다.
it('GET /post/:id', async () => {
const board: CreateBoardDto = {
title: 'test',
content: 'test',
star: '{}',
};
const newBoard = (
await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.send(board)
).body;
const response = await request(app.getHttpServer())
.get(`/post/${newBoard.id}`)
.expect(200);
expect(response).toHaveProperty('body');
const { body } = response;
expect(body).toHaveProperty('id');
expect(body.id).toBe(newBoard.id);
expect(body).toHaveProperty('title');
expect(body).toHaveProperty('content');
expect(body).toHaveProperty('like_cnt');
expect(body).toHaveProperty('images');
});
it('GET /post/:id/is-liked', async () => {
const response = await request(app.getHttpServer())
.get(`/post/${post_id}/is-liked`)
.set('Cookie', [`accessToken=${accessToken}`])
.expect(200);
expect(response).toHaveProperty('body');
const { text } = response;
expect(text === 'true' || text === 'false').toBe(true);
});
// (추가 필요) 서버는 사용자의 요청에 따라 글을 수정한다.
it('PATCH /post/:id', async () => {
const board = {
title: 'test',
content: 'test',
star: '{}',
};
const createdBoard = (
await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.send(board)
).body;
expect(createdBoard).toHaveProperty('id');
const id = createdBoard.id;
const toUpdate: UpdateBoardDto = {
title: 'updated',
content: 'updated',
};
const updated = await request(app.getHttpServer())
.patch(`/post/${id}`)
.set('Cookie', [`accessToken=${accessToken}`])
.send(toUpdate)
.expect(200);
expect(updated).toHaveProperty('body');
const updatedBoard = updated.body;
expect(updatedBoard).toHaveProperty('id');
expect(updatedBoard.id).toBe(id);
expect(updatedBoard).toHaveProperty('title');
expect(updatedBoard.title).toBe(toUpdate.title);
expect(updatedBoard).toHaveProperty('content');
expect(updatedBoard.content).toBe(encryptAes(toUpdate.content));
});
it('PATCH /post/:id with images', async () => {
const board = {
title: 'test',
content: 'test',
star: '{}',
};
const createdBoard = (
await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.send(board)
).body;
expect(createdBoard).toHaveProperty('id');
const id = createdBoard.id;
const toUpdate: UpdateBoardDto = {
title: 'updated',
content: 'updated',
};
const updated = await request(app.getHttpServer())
.patch(`/post/${id}`)
.set('Cookie', [`accessToken=${accessToken}`])
.set('Content-Type', 'multipart/form-data')
.field('title', toUpdate.title)
.field('content', toUpdate.content)
.attach('file', Buffer.from(sampleImageBase64, 'base64'), {
filename: 'test_image_updated1.jpg',
contentType: 'image/jpg',
})
.attach('file', Buffer.from(sampleImageBase64, 'base64'), {
filename: 'test_image_updated2.jpg',
contentType: 'image/jpg',
})
.expect(200);
expect(updated).toHaveProperty('body');
const updatedBoard = updated.body;
expect(updatedBoard).toHaveProperty('id');
expect(updatedBoard.id).toBe(id);
expect(updatedBoard).toHaveProperty('title');
expect(updatedBoard.title).toBe(toUpdate.title);
expect(updatedBoard).toHaveProperty('content');
expect(updatedBoard.content).toBe(encryptAes(toUpdate.content));
expect(updatedBoard).toHaveProperty('images');
expect(Array.isArray(updatedBoard.images)).toBe(true);
});
// #45 [06-08] 서버는 좋아요 / 좋아요 취소 요청을 받아 데이터베이스의 데이터를 수정한다.
it('PATCH /post/:id/like', async () => {
const board = {
title: 'test',
content: 'test',
star: '{}',
};
const resCreate = await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.send(board);
const createdBoard = resCreate.body;
expect(createdBoard).toHaveProperty('like_cnt');
const cntBeforeLike = createdBoard.like_cnt;
const resLike = await request(app.getHttpServer())
.patch(`/post/${createdBoard.id}/like`)
.set('Cookie', [`accessToken=${accessToken}`])
.expect(200);
expect(resLike).toHaveProperty('body');
expect(resLike.body).toHaveProperty('like_cnt');
const cntAfterLike = resLike.body.like_cnt;
expect(cntAfterLike).toBe(cntBeforeLike + 1);
});
it('PATCH /post/:id/unlike', async () => {
const board = {
title: 'test',
content: 'test',
star: '{}',
};
const createdBoard = (
await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.send(board)
).body;
const likedBoard = (
await request(app.getHttpServer())
.patch(`/post/${createdBoard.id}/like`)
.set('Cookie', [`accessToken=${accessToken}`])
).body;
const cntBeforeUnlike = likedBoard.like_cnt;
const resUnlike = await request(app.getHttpServer())
.patch(`/post/${createdBoard.id}/unlike`)
.set('Cookie', [`accessToken=${accessToken}`])
.expect(200);
expect(resUnlike).toHaveProperty('body');
expect(resUnlike.body).toHaveProperty('like_cnt');
const cntAfterUnlike = resUnlike.body.like_cnt;
expect(cntAfterUnlike).toBe(cntBeforeUnlike - 1);
});
// (추가 필요) 서버는 사용자의 요청에 따라 글을 삭제한다.
it('DELETE /post/:id', async () => {
const board: CreateBoardDto = {
title: 'test',
content: 'test',
star: '{}',
};
const newBoard = (
await request(app.getHttpServer())
.post('/post')
.set('Cookie', [`accessToken=${accessToken}`])
.send(board)
).body;
await request(app.getHttpServer())
.delete(`/post/${newBoard.id}`)
.set('Cookie', [`accessToken=${accessToken}`])
.expect(200);
await request(app.getHttpServer()).get(`/post/${newBoard.id}`).expect(404);
});
it('GET /post/:id', async () => {
const { body } = await request(app.getHttpServer())
.get(`/post/${post_id}`)
.expect(200);
expect(body).toHaveProperty('id');
expect(body.id).toBe(post_id);
});
afterEach(async () => {
// 로그아웃
await request(app.getHttpServer())
.post('/auth/signout')
.set('Cookie', [`accessToken=${accessToken}`]);
await app.close();
});
});
대충 이런 흐름임2
© 2023 debussysanjang
- 🐙 [가은] Three.js와의 설레는 첫만남
- 🐙 [가은] JS로 자전과 공전을 구현할 수 있다고?
- ⚽️ [준섭] NestJS 강의 정리본
- 🐧 [동민] R3F Material 간단 정리
- 👾 [재하] 만들면서 배우는 NestJS 기초
- 👾 [재하] GitHub Actions을 이용한 자동 배포
- ⚽️ [준섭] 테스트 코드 작성 이유
- ⚽️ [준섭] TypeScript의 type? interface?
- 🐙 [가은] 우리 팀이 Zustand를 쓰는 이유
- 👾 [재하] NestJS, TDD로 개발하기
- 👾 [재하] AWS와 NCP의 주요 서비스
- 🐰 [백범] Emotion 선택시 고려사항
- 🐧 [동민] Yarn berry로 모노레포 구성하기
- 🐧 [동민] Vite, 왜 쓰는거지?
- ⚽️ [준섭] 동시성 제어
- 👾 [재하] NestJS에 Swagger 적용하기
- 🐙 [가은] 너와의 추억을 우주의 별로 띄울게
- 🐧 [동민] React로 멋진 3D 은하 만들기(feat. R3F)
- ⚽️ [준섭] NGINX 설정
- 👾 [재하] Transaction (트랜잭션)
- 👾 [재하] SSH 보안: Key Forwarding, Tunneling, 포트 변경
- ⚽️ [준섭] MySQL의 검색 - LIKE, FULLTEXT SEARCH(전문검색)
- 👾 [재하] Kubernetes 기초(minikube), docker image 최적화(멀티스테이징)
- 👾 [재하] NestJS, 유닛 테스트 각종 mocking, e2e 테스트 폼데이터 및 파일첨부
- 2주차(화) - git, monorepo, yarn berry, TDD
- 2주차(수) - TDD, e2e 테스트
- 2주차(목) - git merge, TDD
- 2주차(일) - NCP 배포환경 구성, MySQL, nginx, docker, docker-compose
- 3주차(화) - Redis, Multer 파일 업로드, Validation
- 3주차(수) - AES 암복호화, TypeORM Entity Relation
- 3주차(목) - NCP Object Storage, HTTPS, GitHub Actions
- 3주차(토) - Sharp(이미지 최적화)
- 3주차(일) - MongoDB
- 4주차(화) - 플랫폼 종속성 문제 해결(Sharp), 쿼리 최적화
- 4주차(수) - 코드 개선, 트랜잭션 제어
- 4주차(목) - 트랜잭션 제어
- 4주차(일) - docker 이미지 최적화
- 5주차(화) - 어드민 페이지(전체 글, 시스템 정보)
- 5주차(목) - 감정분석 API, e2e 테스트
- 5주차(토) - 유닛 테스트(+ mocking), e2e 테스트(+ 파일 첨부)
- 6주차(화) - ERD
- 2주차(화) - auth, board 모듈 생성 및 테스트 코드 환경 설정
- 2주차(목) - Board, Auth 테스트 코드 작성 및 API 완성
- 3주차(월) - Redis 연결 후 RedisRepository 작성
- 3주차(화) - SignUpUserDto에 ClassValidator 적용
- 3주차(화) - SignIn시 RefreshToken 발급 및 Redis에 저장
- 3주차(화) - 커스텀 AuthGuard 작성
- 3주차(수) - SignOut시 토큰 제거
- 3주차(수) - 깃헙 로그인 구현
- 3주차(토) - OAuth 코드 통합 및 재사용
- 4주차(수) - NestJS + TypeORM으로 MySQL 전문검색 구현
- 4주차(목) - NestJS Interceptor와 로거
- [전체] 10/12(목)
- [전체] 10/15(일)
- [전체] 10/30(월)
- [FE] 11/01(수)~11/03(금)
- [전체] 11/06(월)
- [전체] 11/07(화)
- [전체] 11/09(목)
- [전체] 11/11(토)
- [전체] 11/13(월)
- [BE] 11/14(화)
- [BE] 11/15(수)
- [FE] 11/16(목)
- [FE] 11/19(일)
- [BE] 11/19(일)
- [FE] 11/20(월)
- [BE] 11/20(월)
- [BE] 11/27(월)
- [FE] 12/04(월)
- [BE] 12/04(월)
- [FE] 12/09(금)
- [전체] 12/10(일)
- [FE] 12/11(월)
- [전체] 12/11(월)
- [전체] 12/12(화)