๋ฏฟ์Œ์œผ๋กœ ๊ฐ€๋Š” Jest

2chan
  • #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๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

  1. offset, limit
  2. 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ํผ ์ด์ƒ์„ ์œ ์ง€ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ ๋‹ค์–‘ํ•œ ํ…Œ์ŠคํŠธ ๊ธฐ๋ฒ•์„ ๋„์ž…ํ•˜๊ณ , ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์„ ํ•ญ์ƒ ์ฐพ์•„๊ฐ€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
๋ฌผ๋ก  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ๋ชจ๋“  ์—๋Ÿฌ์™€ ์˜ˆ์™ธ๋ฅผ ์žก์•„๋‚ด์ง€๋Š” ๋ชปํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ณธ์ธ์ด ์ž‘์„ฑํ•œ ์ฝ”๋“œ์— ๋Œ€ํ•œ ์˜๋„๋œ ํ…Œ์ŠคํŠธ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด ํฐ ์žฅ์ ์ด๊ธฐ ๋•Œ๋ฌธ์— ์‹œ๊ฐ„์ด ์ข€ ๊ฑธ๋ฆฌ๋”๋ผ๋„ ํ…Œ์ŠคํŠธ์— ๋งŽ์€ ์‹œ๊ฐ„์„ ์‚ฌ์šฉํ•˜๋ฉฐ ์—…๋ฌด๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์•ž์œผ๋กœ๋„ ์›๋”์›” ๊ฐœ๋ฐœํŒ€์˜ ๋‹ค์–‘ํ•œ ์—…๋ฌด์†Œ์‹์„ ์ „ํ•ด๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ๋งŽ์€ ๊ธฐ๋Œ€ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ