Skip to content

[재하] 1209(토) 개발기록

박재하 edited this page Dec 10, 2023 · 1 revision

목표

  • 서비스 유닛 테스트, mocking
  • e2e 테스트, 파일 첨부
  • 결과 화면

서비스 유닛 테스트

유닛 테스트와 mocking 개요

비즈니스 로직 전체에 대한 검증은 e2e 테스트에서 커버해주기 때문에, 유닛 테스트에서는 함수 모킹을 통해 서비스 메소드의 에러처리가 잘 수행되는지를 꼼꼼하게 확인해줬다.

board 모듈에는 transaction 제어, 복수개의 서비스 파일, 그리고 TypeORM(MySQL) Repository 뿐만 아니라 Mongoose(MongoDB) Model도 의존성에 포함되어 이것들을 모킹하는 방법을 찾는 게 참 어려웠다(자료가 많이 없다). 많은 시행착오 끝에 성공!

방금 언급한 대상들을 유닛 테스트를 위해 모킹하는 방법을 간단히 정리해봤다.

transaction 콜백 모킹

자료찾기 정말 힘들었다..찡찡

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.spyOnmockImplementation 메소드로 정의해준다. 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은 클래스에 정의된 메소드면 에러 없이 잘 된다.

TypeORM Repository 모킹

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);

요런식으로

Mongoose Model 모킹

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();
	});
});

대충 이런 흐름

e2e 테스트 (jest, supertest), 파일 첨부

jest supertest환경에서 파일 첨부때문에 폼데이터로 요청을 보내야 하는데, 인터넷의 자료들 중에 잘 되는 게 있고 안되는 게 있어서, 현재기준 최신버전에서 잘 동작하는 방법으로 정리해 보았다.

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를 추가시켜주면 된다.

폼 데이터 POST 요청, 파일 첨부

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-12-09 오후 1 13 22 스크린샷 2023-12-09 오후 3 11 18

학습 메모

  1. chain 함수 mocking
  2. mock entity manager for unit test with jest
  3. supertest with form data timeout

소개

규칙

학습 기록

[공통] 개발 기록

[재하] 개발 기록

[준섭] 개발 기록

회의록

스크럼 기록

팀 회고

개인 회고

멘토링 일지

Clone this wiki locally