Contents

Unit Test

Unit Test (by ChatGPT)

์œ ๋‹› ํ…Œ์ŠคํŠธ(Unit test)๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ…Œ์ŠคํŠธ์˜ ํ•œ ์ข…๋ฅ˜๋กœ, ๊ฐœ๋ณ„์ ์ธ ์†Œ์Šค ์ฝ”๋“œ์˜ ์ตœ์†Œ ๋‹จ์œ„์ธ ์œ ๋‹›(์ผ๋ฐ˜์ ์œผ๋กœ ํ•จ์ˆ˜, ๋ฉ”์„œ๋“œ, ํด๋ž˜์Šค ๋“ฑ)์ด ์˜๋„ํ•œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. ์œ ๋‹› ํ…Œ์ŠคํŠธ๋Š” ์†Œ์Šค ์ฝ”๋“œ์˜ ํŠน์ • ๋ถ€๋ถ„์„ ๊ฒฉ๋ฆฌ์‹œ์ผœ ํ…Œ์ŠคํŠธํ•˜๋ฏ€๋กœ, ๋‹ค์–‘ํ•œ ์ž…๋ ฅ๊ฐ’๊ณผ ์ƒํ™ฉ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜์—ฌ ์ฝ”๋“œ์˜ ์ •ํ™•์„ฑ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ, ๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋“ฑ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์œ ๋‹› ํ…Œ์ŠคํŠธ์˜ ๋ชฉ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  1. ์ฝ”๋“œ์˜ ์ •ํ™•์„ฑ ๊ฒ€์ฆ: ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ๊ฐ€ ์˜๋„ํ•œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์—ฌ ๋ฒ„๊ทธ์™€ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ณ  ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. ์ฝ”๋“œ์˜ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ: ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ, ๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋“ฑ์„ ํ™•์ธํ•˜์—ฌ ์ฝ”๋“œ์˜ ์•ˆ์ •์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  3. ๋ฆฌํŒฉํ† ๋ง ์ง€์›: ์œ ๋‹› ํ…Œ์ŠคํŠธ๋Š” ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•  ๋•Œ ๊ธฐ์กด ๊ธฐ๋Šฅ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ ์†๋„ ํ–ฅ์ƒ: ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋ฒ„๊ทธ๋ฅผ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋ฏ€๋กœ, ์ƒ์‚ฐ์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  4. ํ˜‘์—…๊ณผ ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด์„ฑ ํ–ฅ์ƒ: ์œ ๋‹› ํ…Œ์ŠคํŠธ๋Š” ์ฝ”๋“œ์˜ ๋™์ž‘์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ฌธ์„œํ™”ํ•˜๊ณ , ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ์ข‹์€ ์œ ๋‹› ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๋น ๋ฅด๊ฒŒ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋ฉฐ, ์ •ํ™•ํ•˜๊ณ  ๋ช…ํ™•ํ•œ ๊ฒ€์ฆ ๋กœ์ง์„ ๊ฐ€์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์œ ๋‹› ํ…Œ์ŠคํŠธ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž‘์„ฑํ•˜๋ฉฐ, ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์˜ ํ’ˆ์งˆ๊ณผ ์•ˆ์ •์„ฑ์„ ๋†’์ด๋Š” ์ค‘์š”ํ•œ ๋„๊ตฌ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

Example

๋ณดํ†ต ๋Œ€๋ถ€๋ถ„์˜ ์„œ๋น„์Šค์— ์กด์žฌํ•˜๋Š”, Authentication/ User service ๊ฐ€ ๋‚ด๊ฐ€ ์˜๋„ํ•˜๋Š”๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ unit test๋ฅผ ์ž‘์„ฑํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

์‹ค์ œ ๊ฐ์ฒด๋ฅผ ์„ ์–ธํ•˜๋ฉด์„œ ์œ ๋‹›ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
describe('The AuthenticationService', () => {
  let authenticationService : AuthenticationService;
  beforeEach(()=>{
    authenticationService = new AuthenticationService(
      new UsersService(
        new Repository<User>()
      ),
      new JwtService({
        secretOrPrivateKey:"key"
      }),
      new ConfigService(),
    );
  })
  
  describe('when creating a cookie', () => {
    it('should return a string', () => {
      const userId = 1;
      expect(
        typeof authenticationService.getCookieWithJwtToken(userId)
      ).toEqual('string')
    })
  })
});

์œ„์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ•  ๊ฒฝ์šฐ, Authentication Service์— ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•  ๋•Œ๋งˆ๋‹ค ํ•„์š”ํ•œ dependency๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋„ฃ์–ด์ค˜์•ผ ํ•˜๋ฏ€๋กœ, ์˜ฌ๋ฐ”๋ฅธ ์ž‘์„ฑ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ๋‹ค.


dependency๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋„ฃ์–ด์•ผ ํ•˜๋Š” ์ด์Šˆ ํ•ด๊ฒฐ

๊ทธ๋Ÿฌ๋ฏ€๋กœ, ์šฐ๋ฆฌ๋Š” @nestjs/testing ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ Test.createTestingModule().compile() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด dependency๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋„ฃ์–ด์ค˜์•ผ ํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
describe('The AuthenticationService', () => {
  let authenticationService : AuthenticationService;
  beforeEach(async ()=>{
    const module = await Test.createTestingModule({
      imports: [
        UsersModule,
        ConfigModule.forRoot({
          validationSchema: Joi.object({
            POSTGRES_HOST: Joi.string().required(),
            POSTGRES_PORT: Joi.number().required(),
            POSTGRES_USER: Joi.string().required(),
            POSTGRES_PASSWORD: Joi.string().required(),
            POSTGRES_DB: Joi.string().required(),
            JWT_SECRET: Joi.string().required(),
            JWT_EXPIRATION_TIME: Joi.string().required(),
            PORT: Joi.number(),
          })
        }),
        DatabaseModule,
        JwtModule.registerAsync({
          imports: [ConfigModule],
          inject: [ConfigService],
          useFactory: async (configService: ConfigService) => ({
            secret: configService.get('JWT_SECRET'),
            signOptions: {
              expiresIn: `${configService.get('JWT_EXPIRATION_TIME')}s`,
            },
          }),
        }),
      ],
      providers: [
        AuthenticationService
      ],
    }).compile();
    authenticationService = await module.get<AuthenticationService>(AuthenticationService);
  })
  
  describe('when creating a cookie', () => {
    it('should return a string', () => {
      const userId = 1;
      expect(
        typeof authenticationService.getCookieWithJwtToken(userId)
      ).toEqual('string')
    })
  })
});

์‹ค์ œ DB๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  DB mock ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ

์œ„์™€ ๊ฐ™์ด Test Module ํ•„์š”ํ•œ module๋“ค์„ ๋ฏธ๋ฆฌ importํ•ด์„œ dependency๋ฅผ ๋งค๋ฒˆ ์ˆ˜๋™์œผ๋กœ ๋„ฃ์–ด์ค˜์•ผ ํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ๋Š” ์žˆ์ง€๋งŒ, ํ˜„์žฌ import๋˜๋Š” ๋ชจ๋“ˆ ์ค‘ Database Module์€ ์‹ค์ œ DB๋ฅผ ์˜๋ฏธํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ์— ์ ํ•ฉํ•˜์ง€ ์•Š๋‹ค. ๋”ฐ๋ผ์„œ ์šฐ๋ฆฌ๋Š” DB Module์„ mocking ํ•ด์„œ provider๋กœ ์ œ๊ณตํ•ด์ค˜์•ผ ํ•œ๋‹ค.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
describe('The AuthenticationService', () => {
  let authenticationService : AuthenticationService;
  beforeEach(async ()=>{
    const module = await Test.createTestingModule({
      imports: [
        UsersModule,
        ConfigModule.forRoot({
          validationSchema: Joi.object({
            POSTGRES_HOST: Joi.string().required(),
            POSTGRES_PORT: Joi.number().required(),
            POSTGRES_USER: Joi.string().required(),
            POSTGRES_PASSWORD: Joi.string().required(),
            POSTGRES_DB: Joi.string().required(),
            JWT_SECRET: Joi.string().required(),
            JWT_EXPIRATION_TIME: Joi.string().required(),
            PORT: Joi.number(),
          })
        }),
        JwtModule.registerAsync({
          imports: [ConfigModule],
          inject: [ConfigService],
          useFactory: async (configService: ConfigService) => ({
            secret: configService.get('JWT_SECRET'),
            signOptions: {
              expiresIn: `${configService.get('JWT_EXPIRATION_TIME')}s`,
            },
          }),
        }),
      ],
      providers: [
        UsersService,
        AuthenticationService,
        {
          provide:getRepositoryToken(User),
          useValue:{}
        }
      ],
    }).compile();
    authenticationService = await module.get<AuthenticationService>(AuthenticationService);
  })
  
  describe('when creating a cookie', () => {
    it('should return a string', () => {
      const userId = 1;
      expect(
        typeof authenticationService.getCookieWithJwtToken(userId)
      ).toEqual('string')
    })
  })
});

์œ„์™€ ๊ฐ™์ด mocked User Repository๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด

1
2
3
4
{
    provide:getRepositoryToken(User),
    useValue:{}
}

ํ•ด๋‹น ๊ฐ์ฒด๋ฅผ provider์— ์ œ๊ณตํ•ด์คฌ๋‹ค. ์ถ”๊ฐ€๋กœ, UserRepository๋Š” User Module์—์„œ TypeormModule.forFeature([User])๋กœ import ๋˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, UserService๋„ provider๋กœ ์ œ๊ณตํ•œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

ConfigService / JwtService์— ๋Œ€ํ•œ mock ๊ฐ์ฒด ์ƒ์„ฑ

์—ฌ๊ธฐ์„œ ๋” ๋‚˜์•„๊ฐ€์„œ, ConfigModule ๊ณผ JwtModule์„ ์ง์ ‘ importํ•˜๋Š”๊ฒŒ ์•„๋‹Œ, ๊ฐ๊ฐ์„ mock ๊ฐ์ฒด๋กœ ์ƒ์„ฑํ•ด์„œ ์ด๋ฅผ provider๋กœ ์ œ๊ณตํ•˜๋Š” ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ฒ ๋‹ค.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const mockedConfigService = {
  get(key: string) {
    switch (key) {
      case 'JWT_EXPIRATION_TIME':
        return '3600'
    }
  }
}

const mockedJwtService = {
  sign: () => ''
}

describe('The AuthenticationService', () => {
  let authenticationService : AuthenticationService;
  beforeEach(async ()=>{
    const module = await Test.createTestingModule({
      providers: [
        UsersService,
        AuthenticationService,
        {
          provide:getRepositoryToken(User),
          useValue:{}
        },
        {
          provide:ConfigService,
          useValue: mockedConfigService
        },
        {
          provide:JwtService,
          useValue:mockedJwtService
        }
      ],
    }).compile();
    authenticationService = await module.get<AuthenticationService>(AuthenticationService);
  })
  
  describe('when creating a cookie', () => {
    it('should return a string', () => {
      const userId = 1;
      expect(
        typeof authenticationService.getCookieWithJwtToken(userId)
      ).toEqual('string')
    })
  })
});

Jest.fn() ์„ ์ด์šฉํ•ด mock ๊ฐ์ฒด ์ƒ์„ฑ

User Service๋„ jest.fn() ์„ ์ด์šฉํ•ด mock ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์„œ dependency๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
describe('The UsersService', () => {
  let usersService: UsersService;
  let findOne: jest.Mock;
  beforeEach(async () => {
    findOne = jest.fn();
    const module = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: {
            findOne
          }
        }
      ],
    })
      .compile();
    usersService = await module.get(UsersService);
  })
  describe('when getting a user by email', () => {
    describe('and the user is matched', () => {
      let user: User;
      beforeEach(() => {
        user = new User();
        findOne.mockReturnValue(Promise.resolve(user));
      })
      it('should return the user', async () => {
        const fetchedUser = await usersService.getByEmail('test@test.com');
        expect(fetchedUser).toEqual(user);
      })
    })
    describe('and the user is not matched', () => {
      beforeEach(() => {
        findOne.mockReturnValue(undefined);
      })
      it('should throw an error', async () => {
        await expect(usersService.getByEmail('test@test.com')).rejects.toThrow();
      })
    })
  })
});