๋ฏฟ์์ผ๋ก ๊ฐ๋ Jest
- #test coverage
- #jest
- #test
- #nest
- #unit test
Jest
NestJs์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํ ์คํ ํด์ธ Jest ํ๋ ์์ํฌ์ @nestjs/testing์ด๋ผ๋ ํจํค์ง๋ฅผ ์ ๊ณตํด์ค๋๋ค.
๊ทธ๋์ NestJs๋ฅผ ์ฌ์ฉํ๋ฉด ๋ณ๋์ ์์นญ ์์ด Jest๋ฅผ ์ฌ์ฉํ์ฌ ํ ์คํธ๋ฅผ ์งํํ ์ ์์ต๋๋ค.
์ด ๊ธ์์๋ Jest๋ฅผ ํตํ ์ฌ๋ฌ๊ฐ์ง ํ ์คํธ ๊ธฐ๋ฒ๊ณผ Coverage์ ๋ํด ์ค๋ช ํ๊ฒ ์ต๋๋ค.
ํ ์คํธ์ฝ๋
ํ ์คํธ๋ ์ค์ํ๊ณ ์ฅ์ ๋ ๋งค์ฐ ๋ง์ต๋๋ค.
์ผ๋จ ๊ฐ๋ฐ์ ์์ ์๊ฒ ์์ ๊ฐ์ ์๊ฒจ์ค๋ค๋ ์ ์ด ๊ฐ์ฅ ํฌ๋ค๊ณ ์๊ฐํ๊ณ ์ด๋ ์ฝ๋์ ์์ ์ฑ ์ฆ๊ฐ, ์์ฐ์ฑ์ ๋์ฌ์ค ์ ์์ต๋๋ค.
๋ฆฌํฉํ ๋ง ์ ์ฒ์๋ถํฐ ์ ๋ถ ํ ์คํธ ํ ํ์ ์์ด ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ํ ์คํธ๋ฅผ ์งํํ ์ ์์ต๋๋ค.
๋ํ ํ ์คํธ ๋ฌธ์๋ฅผ ๋์ฒดํ ์๋ ์์ต๋๋ค.
์ฅ์ ๋ ๋ง์ง๋ง ๋จ์ ๋ ์กด์ฌํ๋๋ฐ, ์๊ฐ๋ณด๋ค ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ์ ์ด ๋จ์ ์ค ํ๋์ ๋๋ค.
ํด๋ณด์ ๋ถ์ด๋ผ๋ฉด ์์๊ฒ ์ง๋ง, ์๋น์ค๋ฅผ ๊ตฌํํ๋ ๊ฒ๋ณด๋ค ๊ทธ ์๋น์ค์ ๋ํ ํ ์คํธ ์งํ, ์์ธ์ผ์ด์ค ์์ฑ ๋ฑ ํ ์คํธ์ฝ๋๋ฅผ ์ง๋ ์๊ฐ์ด ๋ ์ค๋ ์์๋ฉ๋๋ค.
ํ ์คํธ๋ผ๋ฆฌ์ ์์กด์ฑ์ด ๊ฐํ๊ฒ ๊ฒฐํฉํ๋ค๊ฑฐ๋ ํ๋ ์ฝ๋๊ฐ ์๊ธฐ๋ฉด ๋๋ฒ๊น ์ด ์คํ๋ ค ๋ ํ๋ค์ด์ง ์๋ ์์ต๋๋ค.
ํ์ง๋ง ๋จ์ ๋ณด๋ค๋ ์ฅ์ ์ด ํจ์ฌ ๋ ๋ง๋ค๊ณ ์๊ฐํฉ๋๋ค.
ํ ์คํธ ๊ตฌ์ฑ (spec.ts)
์์์ ๋ง์๋๋ฆฐ๊ฒ์ฒ๋ผ NestJs๋ @nesting/testing ํจํค์ง๋ฅผ ์ ๊ณตํฉ๋๋ค.
์ด ํจํค์ง๋ฅผ ์ฌ์ฉํ์ฌ app์ ๋ง๋ค์ด ์ฃผ๊ณ database์ ๊ฒฝ์ฐ sqlite3๋ฅผ ์ฌ์ฉํฉ๋๋ค.
insert, update test๋ฅผ ์คํํด์ผ ํ๊ธฐ ๋๋ฌธ์ ์ค์ db๋ ์ฌ์ฉํ์ง ์์ต๋๋ค.
์ ๋ ํ
์คํธ๋ฅผ ์งํํ ๋๋ controller๋ฅผ ํธ์ถํด์ ํ
์คํธํ๋ฏ๋ก app์์ controller๋ฅผ ๊ฐ์ ธ์์ ํ ๋นํด์ค๋๋ค.
import {Test, TestingModule} from '@nestjs/testing';
describe('WonderwallController', () => {
let app = TestingModule;
// ...
beforeAll(async () => {
app = await Test.createTestingModule({
imports: [
LoggerModule.forRoot(),
ConfigModule.forRoot({
load: [configuration],
cache: true,
isGlobal: true
}),
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
autoLoadEntities: true,
synchronize: true,
logging: false
}),
WonderwallModule
]
});
wonderwallController = app.get(WonderwallController);
});
});
ํ ์คํธ ๊ตฌ์ฑ (seed.ts)
ํ
์คํธ๋ฅผ ์งํํ ๋ ์๋ง์ ์ผ์ด์ค๋ค์ด ์กด์ฌํฉ๋๋ค. ์๋ฅผ ๋ค์ด ์๋์ ์ฝ๋๋ง ๋ด๋ ์ด ํ์ค์๋ง ์ผ์ด์ค๊ฐ 4๊ฐ์ง๋ ๋์ค๊ฒ ๋ฉ๋๋ค.
if (!episode.is_free && !(await this.episodeRepository.hasAccessibility(vod.id, userId))) throw new UnauthorizedException();
์ด๋ฐ ์ฌ๋ฌ ์ผ์ด์ค๋ฅผ ํ
์คํธํ๊ธฐ ์ํด์ ๋ฐ์ดํฐ๋ฅผ ํ๋ฒ ๋ง๋ค๊ณ ๊ทธ ๋ฐ์ดํฐ๋ฅผ ๊ณ์ ์ฌ์ฉํ๋ ๊ฒ ์๋๋ผ, ์ผ์ด์ค๋ณ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฆฌํด์ ์์ฑํ๊ณ ์ญ์ ํ๊ธฐ๋ฅผ ๋ฐ๋ณตํฉ๋๋ค.
๊ทธ๋์ seed.ts๋ผ๋ ํ์ผ์ ๋ง๋ค๊ณ , ์ด ํ์ผ ๋ด๋ถ์ ์๋์ฒ๋ผ ํด๋น ์ผ์ด์ค์ ํ์ํ ๋ชจ๋ ๋ฐ์ดํฐ๋ค์ ์์ฑํด๋๋ ๊ณณ์ผ๋ก ํ์ฉํ์ต๋๋ค.
- seed.ts
// ์ํผ์๋๊ฐ ์ ๋ฃ์ธ ๊ฒฝ์ฐ + ๊ถํ์ด ์๋ ๊ฒฝ์ฐ -> ์คํจ ์ผ์ด์ค์ ๋ฐ์ดํฐ
export const seedGetVodEpisodeVideoPaidVodAndNotAccessible = {
stage: (): Partial<Stage> => ({
title: 'stage title ',
// ...
}),
stageVod: (stageId: number): Partial<StageVod> => ({
title: 'stage_stageVod_title',
// ...
}),
stageVodEpisode: (stageVodId: number): Partial<StageVodEpisode> => ({
title: 'stage_vod_episode_title',
// ...
};
์๋๋ ํ
์คํธ ํ์ผ์
๋๋ค.
ํ
์คํธ ์ผ์ด์ค(it)๋ณ๋ก ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๊ณ ํ
์คํธ๊ฐ ๋๋๋ฉด ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํฉ๋๋ค.
์ด๋ฌํ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ์ผ์ด์ค๋ฅผ ๋งค์ฐ ์ ํํ๊ฒ ํ
์คํธํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
// ์ ๋ฃ์ธ ๊ฒฝ์ฐ + accessible์ด ์๋ ๊ฒฝ์ฐ -> ์คํจ
it('should return UnauthorizedException if vod is not free and user does not have accessibility', async () => {
// ํ
์คํธ ์ ๋ฐ์ดํฐ ์์ฑ
stage = await connection.getRepository(Stage).save(seedGetVodEpisodeVideoPaidVodAndNotAccessible.stage());
// ..
// ํ
์คํธ ์คํ
await expect(episodeController.getVodEpisodeVideo(me[0], {episodeId: stageVodEpisode.id})).rejects.toThrow(UnauthorizedException);
// ํ
์คํธ ์ดํ ๋ฐ์ดํฐ ์ญ์
// ..
await connection.getRepository(Stage).clear();
});
Api test
์๋ ์ฝ๋๋ ์์๋ก ๋ง๋ ์๋์์ ํด๋์ค๋ฅผ ๋ชจ๋ ์กฐํํ๋ GET API์ ์ฟผ๋ฆฌ ๋ถ๋ถ์ ๋๋ค.
getClasses(offset: number, limit: number) {
return this.connection
.getRepository(Class)
.createQueryBuilder('class')
.orderBy({'class.num': 'DESC'})
.take(limit)
.skip(offset)
.getMany();
}
์กฐํ api์์ ์ฃผ๋ก ํ
์คํธ ํ ๋ด์ฉ์ 2๊ฐ์ง์
๋๋ค.
- offset, limit
- order by asc, desc
ํ
์คํธ์ ์์ seed.ts์ ํด๋น ํ
์คํธ๋ฅผ ์ํ ๋ฐ์ดํฐ๋ฅผ ๋จผ์ ์์ฑํฉ๋๋ค.
order by ํ
์คํธ๋ ๋ฐ์ดํฐ ํ๋๊ฐ๋ก ์งํํ๊ธฐ์ ์ ๋ขฐ๋๊ฐ ๋งค์ฐ ๋จ์ด์ง๊ธฐ ๋๋ฌธ์ ์๋์ ๊ฐ์ด ํด๋์ค 100๊ฐ๋ฅผ ์์ฑํฉ๋๋ค.
id๋ auto increments์ด๋ฏ๋ก ์๋ ์์ฑ๋ฉ๋๋ค.
export const seedClasses: Partial<Class>[] = [...new Array(100).keys()].map(i => ({
title: `title_${i}`,
num: Math.floor(Math.random() * 50),
active: i % 3 === ? 'active' : 'inactive'
}));
๋ฐ์ดํฐ๋ฅผ ๋ง๋ค์์ผ๋ ํ
์คํธ๋ฅผ ์งํํฉ๋๋ค.
์ฒซ๋ฒ์งธ๋ก offset, limit์ด querystring์ผ๋ก ์ ๋ฌ๋๋๋ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋์ง ํ
์คํธํฉ๋๋ค.
์๋์์ offset: 0, limit: 7 ์ด๋ฏ๋ก result์ ๊ธธ์ด๋ 7์ด ๋์ผ ํฉ๋๋ค.
it('should return classes according to offset, limit', async () => {
const result = await wonderwallController.getClasses({offset: 0, limit: 7}); // 7๊ฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.
expect(result).toBeDefined();
expect(result).toHaveLength(7); // result์ length๋ 7
// ...
});
๋๋ฒ์งธ๋ num desc ์ ๋ ฌ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋๋์ง ํ
์คํธํฉ๋๋ค.
api ํธ์ถ์ ๊ฒฐ๊ณผ์ธ result์ id๋ก ์ฐ๋ฆฌ๊ฐ ๋ง๋ ํ
์คํธ ๋ฐ์ดํฐ์์ class๋ฅผ ํ๋์ฉ ์ฐพ์ต๋๋ค.
result๋ num desc ์ ๋ ฌ์ด๊ธฐ ๋๋ฌธ์ for๋ฌธ ๋ด๋ถ์์ result์ id๋ก ์ฐพ์ class๋ฅผ ํ๋์ฉ ๋น๊ตํ๋ฉด
ํ์ฌ ๋ฐ์ดํฐ์ num์ด ๋ค์ num๋ณด๋ค ํฌ๊ฑฐ๋ ๊ฐ์์ผ ํฉ๋๋ค.
it('should return classes order by id desc', async () => {
const result = await wonderwallController.getClasses({offset: 0, limit: 9});
for (let i = 0; i < result.length - 1; i++) {
const currentClass = seedClasses.find(({id}) => id === result[i].id);
const nextClass = seedClasses.find(({id}) => id === result[i + 1].id);
expect(currentClass!.num).toBeGreaterThanOrEqual(nextClass!.num);
}
// ...
});
๋ง์ฝ order by column์ด ์ฌ๋ฌ๊ฐ๋ผ๋ฉด ์ ๋ถ ํ
์คํธ๋ฅผ ์งํํด ์ฃผ๋ ๊ฒ์ด ์ข๊ณ
header๋ param์ ์กด์ฌ์ฌ๋ถ์ ๋ฐ๋ผ api์ ์๋์ด ๋ฌ๋ผ์ง๊ฒ ๋๋ค๋ฉด ์ด ๋ํ ํ
์คํธ๋ฅผ ์งํํด์ฃผ๋ ๊ฒ์ด ์์ ํฉ๋๋ค.
SpyOn
Jest์ ์ฅ์ ์ค ํ๋๋ mock ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค๋ ๊ฒ์ ๋๋ค.
jest.spyon() ํจ์๋ฅผ ์ฌ์ฉํด์ ๊ฐ์ง ํจ์๋ฅผ ์์ฑํ ์ ์๋๋ฐ,
์ฃผ๋ก ํจ์์ ๋์์ ๋ค๋ฅด๊ฒ ํ๊ฑฐ๋, ์ค์ ๋ก ์คํ๋๋ฉด ์๋๋ ํจ์์ ๋ํด ์ ์ฉํฉ๋๋ค.
์๋ฌ๊ฐ ๋ฐ์ํ์ ๋ ์ฌ๋์ ์ ์กํด์ฃผ๋ ํจ์๊ฐ ์๋ค๋ฉด, ํ ์คํธ๊ฐ ์คํ๋ ๋๋ ์ ์กํ ํ์๊ฐ ์์ผ๋ฏ๋ก ์๋์ฒ๋ผ ์์ ํฉ๋๋ค.
const spySlack = jest.spyOn(wonderwallService, 'sendSlackFunction').mockImplementation(() => Promise.resolve());
wonderwallService์ sendSlackFunction()์ ๋์์ ๋ณ๊ฒฝ, ์คํ๋ง ํ๊ณ ์๋ฌด ์๋์ ํ์ง ์๊ฒ ๋ณ๊ฒฝํด์ค๋๋ค.
spyon์ผ๋ก ํจ์๋ฅผ mockingํ๋ค๊ณ ํด๋น ํจ์๊ฐ ์คํ๋์ง ์๋ ๊ฒ์ ์๋๋๋ค.
๊ทธ๋ฌ๋ฏ๋ก ํด๋น ํจ์๊ฐ ์คํ๋์๋์ง ํ
์คํธํ๋ ๊ฒ์ด ์ ํํ ํ
์คํธ์
๋๋ค.
toBeCalled, toBeCalledTimes ๋ฑ์ ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ํจ์๊ฐ ํธ์ถ๋์๋์ง๋ฅผ ํ
์คํธํ ์ ์์ต๋๋ค.
expect(spySlack).toBeCalledTimes(1);
jest์์๋ ํํ ์๊ฐํ๋ function ๋ง๊ณ ๋ง์ ๊ฒ์ mockingํ ์ ์์ต๋๋ค.
์๋๋ ํน์ ์กฐ๊ฑด์์ ๋ก๊ทธ๋ฅผ ์ฐ๋ ์ฝ๋์
๋๋ค. !wonderwall์ผ๋ logger.info๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋์ง๋ ํ์ธํ ์ ์์ต๋๋ค.
if (!wonderwall) return this.logger.info('no wonderwall!');
jest.fn()์ ์ฌ์ฉํ์ฌ ๊ฐ์ง ํจ์์ธ loggerMock์ ์์ฑํฉ๋๋ค.
์ฐ๋ฆฌ๊ฐ ์ํ๋ logger.info์ ๊ฐ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ถ๋ ฅ๋๋์ง ํ์ธํ๋ ค๋ฉด logger.info์ ๋์์ ๋ณ๊ฒฝํ์ฌ ํ
์คํธ๊ฐ ๊ฐ๋ฅํ๋๋ก ํด์ผํฉ๋๋ค.
๊ทธ๋์ logger.info์ ๋ฐํ๊ฐ์ ์์์ ๋ง๋ loggerMock์ผ๋ก ๋์ฒด๋๋ก ํ ๊ฑด๋ฐ, mockImplementation ์์์ loggerMock์ ์ฆ์์ผ๋ก ๊ตฌํํ ์ ์์ต๋๋ค.
loggerMock์ message ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ ์ถ๋ ฅํ๋ ํจ์๋ก ๊ตฌํํฉ๋๋ค. ์ด๋ฌ๋ฉด logger.info๊ฐ ์ฌ๋ฐ๋ฅธ message๋ฅผ ์ถ๋ ฅํ๋์ง ํ
์คํธํ ์ ์์ต๋๋ค.
์ฌ๊ธฐ์ message๋ logger.info์ ํ๋ผ๋ฏธํฐ์
๋๋ค.
const loggerMock = jest.fn();
const loggerInfoMock = jest.spyOn(wonderwallService['logger'], 'info').mockImplementation(message => loggerMock(message));
// api ์คํ ...
expect(loggerInfoMock).toBeCalledWith('no wonderwall!');
ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง
ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ ์ ์ฒด ์ฝ๋ ์ค ํ
์คํธ๊ฐ ๋ ๋ถ๋ถ๊ณผ ์๋ ๋ถ๋ถ์ ์ ๋ถ ์ข
ํฉํด์ ๊ทธ ๋น์จ์ ์๋ ค์ฃผ๋ ๊ธฐ๋ฅ์
๋๋ค.
์ ํฌ๋ package.json์ script๋ฅผ ๋ฑ๋กํด์ ์ฌ์ฉํฉ๋๋ค.
"script": {
"test:cov": "jest --coverage"
}
์๋ ์ฌ์ง์ ์๋์ ๋ฐฑ์๋ ๋ ํฌ์งํ ๋ฆฌ์ค ํ๋์ ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง์
๋๋ค.
๋ฆฌํฌํธ๋ฅผ ๋ณด๋ฉด Stmts, Branch, Funcs, Lines, Uncovered Line 5๊ฐ์ง๊ฐ ์กด์ฌํฉ๋๋ค.
- Stmts์ Lines๋ ๋น์ทํฉ๋๋ค. ์ ์ฒด ๋ช ๋ น๋ฌธ์ด ์ผ๋ง๋ ์ํ๋์๋์ง ๋ณด์ฌ์ค๋๋ค.
- Branch๋ ์ ์ฒด ์ฝ๋์ ๋ถ๊ธฐ๋ฌธ์ ๋ํ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค๋๋ค.
- Funcs ์ ์ฑ ์ฝ๋์ ํจ์ ์คํ์ ๋ํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค๋๋ค.
- Uncovered Line๋ ํ ์คํธ๊ฐ ์คํ๋์ง ์์ ๋ผ์ธ์ ์๋ ค์ค๋๋ค. ๊ฐ์ธ์ ์ผ๋ก ํ ์คํธํ ๋ ๊ฐ์ฅ ์ค์ํ๊ฒ ๋ด์ผํ๋ ๋ถ๋ถ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค.
๋ง์น๋ฉฐ
์๋์ ๋ฐฑ์๋ ํ์ ํ
์คํธ๋ฅผ ์ค์์ํฉ๋๋ค.
ํ
์คํธ ์ฝ๋์์ ์ต๋ํ ์์ธ๋ฅผ ์ก์๋ด๊ณ , ์ค์ ํ๊ฒฝ์์ ํ๊ธฐ ํ๋ ํ
์คํธ๋ฅผ ๊ตฌํํ๋ ค ๋
ธ๋ ฅํฉ๋๋ค.
๋๋ถ๋ถ์ ๋ ํฌ์งํ ๋ฆฌ๊ฐ ์ปค๋ฒ๋ฆฌ์ง 90ํผ ์ด์์ ์ ์งํ๊ณ ์์ผ๋ฉฐ ๋ค์ํ ํ
์คํธ ๊ธฐ๋ฒ์ ๋์
ํ๊ณ , ๋ ์ข์ ๋ฐฉ๋ฒ์ ํญ์ ์ฐพ์๊ฐ๊ณ ์์ต๋๋ค.
๋ฌผ๋ก ํ
์คํธ ์ฝ๋๊ฐ ๋ชจ๋ ์๋ฌ์ ์์ธ๋ฅผ ์ก์๋ด์ง๋ ๋ชปํฉ๋๋ค. ํ์ง๋ง ๋ณธ์ธ์ด ์์ฑํ ์ฝ๋์ ๋ํ ์๋๋ ํ
์คํธ๋ฅผ ์ ํํ๊ฒ ํ ์ ์๋ค๋ ์ ์ด ํฐ ์ฅ์ ์ด๊ธฐ ๋๋ฌธ์ ์๊ฐ์ด ์ข ๊ฑธ๋ฆฌ๋๋ผ๋ ํ
์คํธ์ ๋ง์ ์๊ฐ์ ์ฌ์ฉํ๋ฉฐ ์
๋ฌด๋ฅผ ์งํํ๊ณ ์์ต๋๋ค.
์์ผ๋ก๋ ์๋์ ๊ฐ๋ฐํ์ ๋ค์ํ ์ ๋ฌด์์์ ์ ํด๋๋ฆฌ๊ฒ ์ต๋๋ค. ๋ง์ ๊ธฐ๋ ๋ถํ๋๋ฆฝ๋๋ค.
๊ฐ์ฌํฉ๋๋ค.