avatar
Published on

NestJS ์—์„œ UNIT TEST ๐Ÿƒ๐Ÿปโ€โ™€๏ธ

Authors
  • avatar
    Name
    Haneul
    Twitter

Jest

NestJS ์—์„œ UNIT TEST ๐Ÿƒ๐Ÿปโ€โ™€๏ธ

jwt.service.spec.ts

describe('JwtService', () => {
  let service: JwtService;

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        JwtService,
        { provide: CONFIG_OPTIONS, useValue: jwtOptions },
      ],
    }).compile();
    service = module.get<JwtService>(JwtService);
  });

jwt service ๋Š” jsonwebtoken ํŒจํ‚ค์ง€ ์ด์™ธ์—๋Š” ๋‹ฌ๋ฆฌ mocking ํ•  ๊ฒƒ์ด ์—†๋‹ค. jwt service๊ฐ€ ์ œ๋Œ€๋กœ ์ค€๋น„ ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋ ค๋ฉด

it('should be defined',()=>{ expect(service).toBeDefined();});

์ด ์ฝ”๋“œ๋กœ ํ™•์ธํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.

jest.mock('jsonwebtoken', () => ({
  sign: jest.fn(() => MOCKED_TOKEN),
  verify: jest.fn(() => ({ id: TEST_ID })),
}))

describe('Test sign method', () => {
  it('should return MOCKED_TOKEN', () => {
    const token = service.sign(TEST_ID)

    expect(jwt.sign).toHaveBeenCalledTimes(1)
    expect(jwt.sign).toHaveBeenCalledWith({ id: TEST_ID }, TEST_PRIVATE)
    expect(token).toEqual(MOCKED_TOKEN)
    expect(typeof token).toEqual('string')
  })
})

jest.mock('jsonwebtoken', () => ...) ์ด ๋ธ”๋Ÿญ์€ jsonwebtoken ํŒจํ‚ค์ง€๋ฅผ mockingํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

์œ ๋‹›ํ…Œ์ŠคํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์— jwt service ์ž์ฒด์—๋งŒ ์ง‘์ค‘ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฌํ•œ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ํŒจํ‚ค์ง€๋‚˜ ํ•จ์ˆ˜, ํด๋ž˜์Šค๋“ค์€ mockingํ•˜์—ฌ ํ…Œ์ŠคํŠธ์˜ ๋„๊ตฌ๋กœ ์ด์šฉํ•œ๋‹ค.

๋ฐ‘์— expect(jwt.sign).toHaveBeenCalledTimes(1);์€ jwt.sign์ด ๋‹จ ํ•œ ๋ฒˆ ํ˜ธ์ถœ ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ๋กœ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

expect์™€ ์‹ค์ œ์™€ ๋‹ค๋ฅด๋ฉด ํ…Œ์ŠคํŠธ๋Š” ์‹คํŒจํ•œ๋‹ค.

describe, it ์„ ์ ์ ˆํžˆ ์ž˜ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์ด ๋†’์•„์ง„๋‹ค. describe๋Š” ํ…Œ์ŠคํŠธํ•  ๋Œ€์ƒ์— ๋Œ€ํ•ด ์„ค๋ช…ํ•œ๋‹ค.

it(individual test), ๊ทธ ๋Œ€์ƒ์„ ๊ฐœ๋ณ„์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

users.service.spec.ts

describe('UsersService', () => {
  let service: UsersService;
  let jwtService: JwtService;
  let userRepository: MockRepository<User>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: mockRepository(),
        },
        {
          provide: JwtService,
          useValue: mockJwtService,
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    userRepository = module.get(getRepositoryToken(User));
    jwtService = module.get<JwtService>(JwtService);
  });

๋จผ์ € user service๋งŒ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•˜๋Š”๋ฐ, repository๋„ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๊ณ , user ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” jwt service๋„ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. user service์— ์ง‘์ค‘ํ•ด์•ผ ํ•˜๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์ด๋ฏ€๋กœ, ์ด๊ฒƒ๋“ค์€ mocking์„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. jwtService๋Š” jwt.service์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์—ฌ๊ธฐ์„œ๋„ ๋‹ค์‹œ mocking์„ ํ•ด์ฃผ๊ณ , repository๋Š” ์•ฝ๊ฐ„ ์ƒ์†Œํ•˜์ง€๋งŒ ์œ„์˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด ์„ค์ •์„ ํ•ด์ค๋‹ˆ๋‹ค.

์ฝ”๋“œ๋ฅผ ๋ณด์‹œ๋ฉด jwt.service.spec.ts์™€๋Š” ๋‹ฌ๋ฆฌ users.service.spec.ts์—์„œ๋Š” beforeAll์ด ์•„๋‹ˆ๋ผ beforeEach๋กœ ๋ชจ๋“ˆ์„ ์ดˆ๊ธฐํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋‘˜์˜ ์ฐจ์ด๋Š” ๋ญ˜๊นŒ์š”?

ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ์ „์— ์•ž์˜ jwt ํ…Œ์ŠคํŠธ์—์„œ ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ์„ธํŒ…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„ธํŒ…์„ ๋งค ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ํ•  ๊ฒƒ์ธ์ง€ ๋˜๋Š” ์ „์ฒด ํ…Œ์ŠคํŠธ์—์„œ ํ•œ ๋ฒˆ๋งŒํ•ด๋„ ๋˜๋Š”์ง€์˜ ์ฐจ์ด์ž…๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, mockingํ•œ respository ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” createAccount ํ…Œ์ŠคํŠธ์—๋„ ์‚ฌ์šฉํ•  ์ˆ˜๊ฐ€ ์žˆ๊ณ , seeProfile, editProfile ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒฝ์šฐ์—๋„ ๋ชจ๋‘ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•œ๋ฐ, ๋งค๋ฒˆ ์‚ฌ์šฉํšŸ์ˆ˜๋‚˜ ๋“ค์–ด ๊ฐ„ ๋ณ€์ˆ˜๋“ค์„ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์œผ๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ๊ผฌ์ผ ์ˆ˜ ๋ฐ–์— ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ user.service๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ์—๋Š” beforeAll์ด ์•„๋‹ˆ๋ผ beforeEach๊ฐ€ ๋˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

async createAccount({
    email,
    password,
    role,
  }: CreateAccountInput): Promise<CreateAccountOutput> {
    try {
      const exists = await this.users.findOne({ email });
      // test 1
      if (exists) {
        return { ok: false, error: `There is a user with that email already` };
      }
      const user = this.users.create({
        email,
        password,
        role,
      });
      await this.users.save(user);

      // test 2
      return {
        ok: true,
        error: null,
      };
    } catch {
        // test 3
      return {
        ok: false,
        error: 'Could not create account',
      };
    }
  }

1์˜ ๊ฒฝ์šฐ - ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์ด ์žˆ์„ ๊ฒฝ์šฐ์—๋Š” creatAccount๊ฐ€ ์‹คํŒจํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2์˜ ๊ฒฝ์šฐ - ์ •์ƒ์ ์œผ๋กœ ๊ณ„์ •์ด ๋งŒ๋“ค์–ด์ ธ์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

3์˜ ๊ฒฝ์šฐ - findOne์ด๋‚˜ save ๋ฉ”์†Œ๋“œ์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ catch๋กœ ๋„˜์–ด๊ฐ€๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

์œ„์˜ createAccount์—์„œ๋Š” ์œ„์˜ logic๋“ค์„ ํŒ๋‹จํ•ด์•ผ๋งŒ ์ฝ”๋“œ ๋ชจ๋“  ๋ถ€๋ถ„๋“ค์„ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ…Œ์ŠคํŠธ๋Š”

it('should success to create User', async () => { ...
it('should fail because of user existing', async () => { ...
it('should fail because of saving failed', async () => { ...

์ด๋ ‡๊ฒŒ ์„ธ ๊ฐ€์ง€ ๋กœ์ง์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด createAccount์— ๋Œ€ํ•œ test coverage๊ฐ€ ์ „๋ถ€ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค.

์œ„์˜ it(individual test) ์ฝ”๋“œ๋ฅผ ์กฐ๊ธˆ๋” ์‚ดํŽด ๋ณด๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋–ค ์‹์œผ๋กœ ์ง„ํ–‰์ด ๋˜์–ด์•ผ ํ•˜๋Š”์ง€ ๊ฐ์„ ์žก์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

it('should success to create user', ...

createAccount๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์„ฑ๊ณต์ ์œผ๋กœ ๊ณ„์ • ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. service์˜ ๋กœ์ง๋งŒ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ respository๋Š” ํ‰๋‚ด๋‚ด๊ธฐ๋ฅผ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. users.service.spec.ts ์ƒ๋‹จ ์ฝ”๋“œ์— ๋ณด๋ฉด ์ด๋ฏธ userRepository๋ฅผ ํ‰๋‚ด๋‚ด๊ณ  ์„ค์ •์„ ํ•ด๋†จ์Šต๋‹ˆ๋‹ค. beforeEach๋ฅผ ํ†ตํ•ด์„œ ๋งค ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ดˆ๊ธฐํ™”๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฑฑ์ •ํ•˜์ง€ ์•Š๊ณ  ํ…Œ์ŠคํŠธ ์•ˆ์—์„œ ๋ฆฌํ„ด ๊ฐ’์„ ํ‰๋‚ด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

userRepository.findOne.mockResolvedValueOnce(null);

user repository์˜ findOne๊ฐ’์„ ํ‰๋‚ด๋‚ธ ๊ฒƒ์ž…๋‹ˆ๋‹ค. mockResolvedValue๋Š” Promise๋ฅผ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค. Promise์˜ resolve๊ฐ’์ด null์ด๋ผ๋Š” ๋œป์ด ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ db๋ผ๋ฉด db ์—ฐ๊ฒฐ์ด๋‚˜ ์ƒํƒœ์— ๋”ฐ๋ผ ๊ฐ’์„ ๋ฆฌํ„ด ๋ชปํ•  ์ˆ˜๋„ ์žˆ๊ณ  ์—ฐ๊ฒฐ์ด ์•ˆ๋  ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ผ๋Š” ์˜๋ฏธ๊ฐ€ ์‚ด์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋–ค ๊ฒฝ์šฐ๋ผ๋„ db์™€๋Š” ๊ด€๊ณ„ ์—†์ด null ๊ฐ’์„ resolve๋กœ ๋„˜๊ฒจ์ค€๋‹ค๋Š” ๋œป์ด ๋ฉ๋‹ˆ๋‹ค.

userRepository.create.mockReturnValueOnce(hostArgs);

create๋Š” typeorm repository์—์„œ Promise๋กœ ๋ฆฌํ„ด์„ ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•ด์ค๋‹ˆ๋‹ค. ์„ธ์ด๋ธŒ ๋˜๊ธฐ ์ด์ „ ๊ฐ’์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ create๋Š” resolve ๊ฐ’์„ ๋ฆฌํ„ดํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. ๊ทธ๋ƒฅ ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์œ„์™€ ๊ฐ™์ด ์„ค์ •์„ ํ•ด์ฃผ๋ฉด ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค.

userRepository.save.mockResolvedValue(TEST_HOST);

findOne๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ resolve ๊ฐ’์„ ๋ฆฌํ„ดํ•ด์ค๋‹ˆ๋‹ค. ์„ฑ๊ณต์ ์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ ๊ฒฝ์šฐ์ด๋ฏ€๋กœ db์— ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋œ ๊ฐ’์„ ๋ฆฌํ„ดํ•˜๋Š” repository์˜ save ๋ฉ”์†Œ๋“œ๋ฅผ ํ‰๋‚ด๋‚ธ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

describe('createAccount Testing', () => {
  it('should success to create User', async () => {
    userRepository.findOne.mockResolvedValueOnce(null)
    userRepository.create.mockReturnValueOnce(hostArgs)
    userRepository.save.mockResolvedValue(TEST_HOST)

    const result = await service.createAccount(hostArgs)

    expect(userRepository.findOne).toHaveBeenCalledTimes(1)
    expect(userRepository.findOne).toHaveBeenCalledWith({
      email: hostArgs.email,
    })
    expect(userRepository.create).toHaveBeenCalledTimes(1)
    expect(userRepository.create).toHaveBeenCalledWith(hostArgs)
    expect(userRepository.save).toHaveBeenCalledTimes(1)
    expect(userRepository.save).toHaveBeenCalledWith(hostArgs)
    expect(result).toMatchObject({ ok: true, error: null })
  })
})

createAccount๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ์„ค์ •ํ•ด ๋†“์€ ๊ฐ’์— ์˜ํ•˜๋ฉด ์œ„ ์ฝ”๋“œ๋Š” findOne์„ ํ˜ธ์ถœํ•ด์„œ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ ๊ณ„์ •์ด ์—†์Œ์„ ํ™•์ธํ•˜๊ณ  create ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์„œ entity๋ฅผ ๋งŒ๋“ค๊ณ , save ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์„œ db์— ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋กœ์ง์œผ๋กœ ํ˜๋Ÿฌ ๊ฐˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  createAccount๊ฐ€ ์œ„์˜ ๋กœ์ง๋Œ€๋กœ ํ˜๋Ÿฌ๊ฐ„๋‹ค๊ณ  ๊ธฐ๋Œ€ํ•˜์˜€์œผ๋ฏ€๋กœ, ์‹ค์ œ๋กœ ๊ทธ๋ ‡๊ฒŒ ํ˜ธ์ถœ์ด ๋˜์—ˆ๋‚˜ ํ™•์ธ๋งŒ ํ•ด์ฃผ๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์œ„์˜ ๋กœ์ง๋Œ€๋กœ findOne์ด ํ˜ธ์ถœ๋˜์—ˆ๋Š”๊ฐ€(toHaveBeenCalledTimes), ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์ œ๋Œ€๋กœ ์ž…๋ ฅ ๋˜์—ˆ๋Š”๊ฐ€(toHaveBeenCalledWith), create๋Š” ์ œ๋Œ€๋กœ ํ˜ธ์ถœ๋˜์—ˆ๋Š”๊ฐ€, save๋Š” ์ œ๋Œ€๋กœ ํ˜ธ์ถœ๋˜์—ˆ๊ณ , ๊ฒฐ๊ณผ๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๊ฐ’(toMatchObject)์ด ๋‚˜์˜ค๋Š”๊ฐ€?๋ฅผ jest์—์„œ ํŒ๋‹จ์„ ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— createAccount๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋Š” ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

createAccount๋ฅผ ๋ชจ๋‘ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด, ์•ž์—์„œ ์–ธ๊ธ‰๋“œ๋ฆฐ ๋Œ€๋กœ, ์ด๋ฏธ ์ด๋ฉ”์ผ ๊ณ„์ •์ด ์ด๋ฏธ ์žˆ๋Š” ๊ฒฝ์šฐ์™€, save ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํŒจํ•  ๊ฒฝ์šฐ๋ฅผ ๋ชจ๋‘ ํ…Œ์ŠคํŠธํ•ด์•ผ full coverage๋ฅผ ์™„์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ  : jset ๊ณต์‹๋ฌธ์„œ, jset mocking,