๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
43-Testing

1. ํ…Œ์ŠคํŒ…

ํ…Œ์ŠคํŒ…์€ ์†Œํ”„ํŠธ์›จ์–ด๊ฐ€ ์˜ˆ์ƒํ•œ๋Œ€๋กœ ์‹คํ–‰๋˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๊ณ  ํ™•์ธํ•˜๋Š” ๊ณผ์ •์ด๋‹ค

ํ…Œ์ŠคํŒ…์˜ ์ค‘์š”์„ฑ

  • QA(Quality Assurance) : ๋ฏธ๋ฆฌ ๋ฒ„๊ทธ๋ฅผ ๋ฐœ๊ฒฌํ•ด์„œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š”๊ฑธ ๋ฐฉ์ง€ํ•œ๋‹ค
  • ๋”์šฑ ๋น ๋ฅธ ๋ฆฌํŒฉํ„ฐ๋ง : ๋ณ€๊ฒฝํ•œ ์ฝ”๋“œ๊ฐ€ ๊ธฐ์กด ๋กœ์ง์—์„œ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ฌ๊ฒฝ์šฐ ๋ฏธ๋ฆฌ ์•Œ ์ˆ˜ ์žˆ๋‹ค
  • ๋‹คํ๋ฉ˜ํ…Œ์ด์…˜ ์—ญํ•  : ํ…Œ์ŠคํŠธ ์ž์ฒด๊ฐ€ ์ฝ”๋“œ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•ด์•ผํ•˜๋Š”์ง€ ์„ค๋ช…ํ•˜๋Š” ํ•˜๋‚˜์˜ ๋ฌธ์„œ ์—ญํ• ์„ ํ•œ๋‹ค
  • ์ฝ”๋“œ ๋กœ์ง ๊ฒ€์ฆ ์ž๋™ํ™” : ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ด์„œ ๋กœ์ง ๊ฒ€์ฆ์„ ์ž๋™ํ™”ํ•˜์—ฌ ์ง์ ‘ ํ”„๋กœ๊ทธ๋žจ์„ ํ…Œ์ŠคํŠธํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง„๋‹ค

1.1 Testing์˜ ์ค‘์š”์„ฑ

1
import { Injectable } from '@nestjs/common'
2
3
@Injectable()
4
export class MathServerice {
5
add(a: number, b: number): number {
6
return a + b
7
}
8
}
  • ํ˜„์žฌ ์šฐ๋ฆฌ์˜ ์ง€์‹์œผ๋กœ ์ด ์ฝ”๋“œ๊ฐ€ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด,
    • ์ด ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๋Š” Controller๋ฅผ ํ†ตํ•ด API ์ฝœ์„ ์ง์ ‘ ํ•˜๋Š” ๋ฐฉ๋ฒ•๋ฐ–์— ์—†๋‹ค.
  • ๋„ˆ๋ฌด ์˜ค๋ž˜๊ฑธ๋ฆฌ๊ณ  ์ผ๊ด€์„ฑ์ด ๋ถ€์กฑํ•˜๋‹ค.
  • ์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์€ ์‚ฌ๋žŒ์€ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธ ํ•ด์•ผํ• ์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค. ์–ด๋–ค ๊ฐ’๋“ค๊นŒ์ง€ ํ…Œ์ŠคํŠธ ํ•ด์•ผํ•˜๋Š”์ง€ ๋“ฑ
  • ์ •ํ™•ํ•œ ๋กœ์ง์˜ ๋ฐ”์šด๋”๋ฆฌ๋ฅผ ์•Œ ์ˆ˜ ์—†์œผ๋‹ˆ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ์ด ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ–ˆ์„๋•Œ,
    • ๋‚ด๊ฐ€ ๋ง๊ฐ€๋œจ๋ฆฐ ๋กœ์ง์ด ์—†๋Š”์ง€ ์‰ฝ๊ฒŒ ํ™•์ธ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” ์ฝ”๋“œ๋ฅผ ์ฝ”๋“œ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ , ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

1.2 Testing ์˜ˆ์ œ

1
import { Test, TestingModule } from '@nestjs/testing'
2
import { MathService } from './math.service'
3
4
describe('MathService', () => {
5
let service: MathService
6
7
beforeEach(async () => {
8
const module: TestingModule = await Test.createTestingModule({
9
providers: [MathService],
10
}).compile()
11
12
service = module.get<MathService>(MathService)
13
})
14
15
it('should be defined', () => {
16
expect(service).toBeDefined() // service๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
17
})
18
19
it('should return correct sum of two numbers', () => {
20
const result = service.add(2, 3)
21
expect(result).toEqual(5) // ์ธํ’‹์— ๋Œ€ํ•œ ์•„์›ƒํ’‹์„ ํ…Œ์ŠคํŠธ
22
})
23
24
it('should handle negative numbers', () => {
25
const result = service.add(-1, 3)
26
expect(result).toEqual(-4) // ๋‹ค์–‘ํ•œ ์ผ€์ด์Šค๋ฅผ ํ…Œ์ŠคํŠธ
27
})
28
})
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์ฝ”๋“œ์˜ ์ž‘๋™ ๋กœ์ง์„ ๊ฒ€์ฆํ•˜๋Š” ์ฝ”๋“œ๋‹ค.
  • ํ”ํžˆ ์–ด๋–ค ํŠน์ • ๊ฐ’์ด ์ž…๋ ฅ๋์„๋•Œ ์–ด๋–ค ๊ฐ’์ด ๋ฐ˜ํ™˜๋˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • ๋‹ค์–‘ํ•œ ๊ฐ’๋“ค์„ ์ž…๋ ฅ ํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ์ œ์—์„œ ์ฒ˜๋Ÿผ ์–‘์ˆ˜ ๋ฟ ๋งŒ ์•„๋‹ˆ๋ผ ์Œ์ˆ˜์— ๋Œ€ํ•œ ๋ง์…ˆ๋„ ํ…Œ์ŠคํŠธ ํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  • ํŠน์ • ๊ธฐ๋Œ€์น˜์— ๋Œ€ํ•œ assert๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.
  • expect()๋ฅผ ์‚ฌ์šฉํ•œ assert๋Š” false๋ฅผ ๋ฐ˜ํ™˜ํ• ๋•Œ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•œ๋‹ค.
  • ์–ต์ง€๋กœ ์ด์ƒํ•œ ๊ฐ’์„ ๋„ฃ์–ด์„œ ๊ธฐ๋Œ€ํ•˜๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ๊ฐ€๋Šฅํ•˜๋ฉด ์•ˆ๋˜๋Š” ๋กœ์ง์„ ๊ฒ€์ฆ ํ•  ์ˆ˜ ์žˆ๋‹ค.

1.3 Testing ๊ฒฐ๊ณผ

1
PASS src/math.service.spec.ts
2
MathService
3
โˆš should be defined (5 ms)
4
โˆš should return correct sum of two numbers (1 ms)
5
โˆš should handle negative numbers (1 ms)
6
7
Test Suites: 1 passed, 1 total
8
Tests: 3 passed, 3 total
9
Snapshots: 0 total
10
Time: 1.015 s
  • pnpm test ์ปค๋งจ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ ๊ฐ’์„ ๋ฐ›์•„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  • ์–ด๋–ค ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณต/์‹คํŒจ ํ–ˆ๋Š”์ง€์™€ ํ•จ๊ป˜ ๋ช‡๊ฐœ์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณต/์‹คํŒจ ํ–ˆ๋Š”์ง€,
    • ์‹คํ–‰ํ•˜๋Š”๋ฐ ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ ธ๋Š”์ง€ ๋“ฑ์˜ ํ†ต๊ณ„ ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค

2. Matcher ํ•จ์ˆ˜

1
test('two plus two is four', () => {
2
// expect() ํ•จ์ˆ˜์˜ ์•„๊ทœ๋จผํŠธ๊ฐ€
3
// toBe() ํ•จ์ˆ˜์˜ ์•„๊ทœ๋จผํŠธ์™€ ๊ฐ™์€์ง€ ํ™•์ธํ•œ๋‹ค
4
expect(2 + 2).toBe(4)
5
})

2.1 ๊ธฐ๋ณธ Matcher

  • toBe(value) : ๊ฐ’์ด ๊ฐ™์€์ง€ ํ™•์ธํ•œ๋‹ค.
  • toEqual(value) : ๊ฐ์ฒด์˜ ๋ชจ๋“  ๊ฐ’์ด ๊ฐ™์€์ง€ ์žฌ๊ท€์ ์œผ๋กœ ํ™•์ธํ•œ๋‹ค.
  • toBeNull() : toBe(null)๊ณผ ๊ฐ™๋‹ค. Falsy์ผ๋•Œ ์กฐ๊ธˆ ๋” ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
  • toBeUndefined() : ๊ฐ’์ด undefined์ธ๊ฑธ ํ™•์ธํ•œ๋‹ค.
    • toBe(undefined)๋ฅผ ์‹คํ–‰ ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ ์ง์ ‘ ์ฝ”๋“œ์—์„œ undefined๋ฅผ ๋ ˆํผ๋Ÿฐ์Šค ํ•˜๋Š” ์ง€์–‘ํ•ด์•ผํ•œ๋‹ค.
  • toBeDefined() : toBeUndefined()์˜ ๋ฐ˜๋Œ€๋‹ค.
  • toBeTruthy() JS์—์„œ ์ธ์ง€ํ•˜๋Š” true ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toBeFalsy() : toBeTruthy()์˜ ๋ฐ˜๋Œ€
  • toBeNan() : ์ˆซ์ž๊ฐ€ ์•„๋‹˜์„ ํ™•์ธํ•œ๋‹ค

2.1.1 toEqual() vs toBe()

1
const can1 = {
2
flavor: 'grapefruit',
3
ounces: 12,
4
}
5
const can2 = {
6
flavor: 'grapefruit',
7
ounces: 12,
8
}
9
10
describe('Test', () => {
11
test('have all the same properties', () => {
12
expect(can1).toEqual(can2)
13
})
14
test('are the exact same can', () => {
15
expect(can1).not.toBe(can2)
16
})
17
})

2.2 ์ˆซ์ž Matcher

  • toBeGreaterThan(number) : ๊ฐ’์ด ๋” ํฐ์ง€ ํ™•์ธํ•œ๋‹ค.
  • toBeGreaterThanOrEqual(number) : ๊ฐ’์ด ๋” ํฌ๊ฑฐ๋‚˜ ๊ฐ™์€์ง€ ํ™•์ธํ•œ๋‹ค.
  • toBeLessThan(number) : ๊ฐ’์ด ๋” ์ž‘์€์ง€ ํ™•์ธํ•œ๋‹ค.
  • toBeLessThanOrEqual(number) : ๊ฐ’์ด ๋” ์ž‘๊ฑฐ๋‚˜ ๊ฐ™์€์ง€ ํ™•์ธํ•œ๋‹ค.
  • toBeCloseTo(number, numDigits?) : ํŠน์ • ์†Œ์ˆ˜์ ๊นŒ์ง€ ๊ฐ™์€ ๊ฐ’์ธ์ง€ ํ™•์ธํ•œ๋‹ค

2.2.1 toBeCloseTo()

1
test('adding works sanely with decimals', () => {
2
// Fall! (0.30000000000000004 !== 0.3)
3
expect(0.2 + 0.1).toBe(0.3)
4
})
1
test('adding works sanely with decimals', () => {
2
expect(0.2 + 0.1).toBeCloseTo(0.3, 5)
3
})

2.3 ํ•จ์ˆ˜ Matcher

  • toHaveBeenCalled() : mock function์ด ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • toHaveBeenCalledTimes(number) : mock function์ด ์ง€์ •๋œ ํšŸ์ˆ˜๋งŒํผ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveBeenCalledWith(arg1, arg2, โ€ฆ) : mock function์ด ํŠน์ • ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ํ•จ๊ป˜ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveBeenLastCalledWith(value) : mock function์ด ๋งˆ์ง€๋ง‰์œผ๋กœ ํ˜ธ์ถœ๋  ๋•Œ ํŠน์ • ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ํ•จ๊ป˜ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveBeenNthCalledWith(nthCall, value) : mock function์ด n๋ฒˆ์งธ๋กœ ํ˜ธ์ถœ๋  ๋•Œ ํŠน์ • ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ํ•จ๊ป˜ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveReturned() : mock function์ด ๊ฐ’์„ ๋ฐ˜ํ™˜ํ–ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. (์—๋Ÿฌ๋ฅผ ๋˜์ง€์ง€ ์•Š์Œ)
  • toHaveReturnedTimes(number) : mock function์ด ๊ฐ’์„ ์ง€์ •๋œ ํšŸ์ˆ˜๋งŒํผ ๋ฐ˜ํ™˜ํ–ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveReturnedWith(value) : mock function์ด ํŠน์ • ๊ฐ’์„ ๋ฐ˜ํ™˜ํ–ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveLastReturnedWith(value) : mock function์ด ๋งˆ์ง€๋ง‰์œผ๋กœ ํŠน์ • ๊ฐ’์„ ๋ฐ˜ํ™˜ํ–ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveNthReturnedWith(nthCall, value) : mock function์ด n๋ฒˆ์งธ๋กœ ํŠน์ • ๊ฐ’์„ ๋ฐ˜ํ™˜ํ–ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค

2.3.1 toHaveBeenCalled()

1
function dringAll(callback, flavour) {
2
if (flavour !== 'octopus') {
3
callback(flavour)
4
}
5
}
6
7
describe('Test', () => {
8
test('drinks something lemon-flavoured', () => {
9
const drink = jest.fn()
10
dringAll(drink, 'lemon')
11
expect(drink).toHaveBeenCalled()
12
})
13
14
test('does not drink something octopus-flavoured', () => {
15
const drink = jest.fn()
16
dringAll(drink, 'octopus')
17
expect(drink).not.toHaveBeenCalled()
18
})
19
})

2.3.2 toHaveNthReturnedWith()

1
test('drink returns expected nth calls', () => {
2
const beverage1 = { name: 'Lemon' }
3
const beverage2 = { name: 'Orange' }
4
const drink = jest.fn(beverage => beverage.name)
5
6
drink(beverage1)
7
drink(beverage2)
8
9
expect(drink).toHaveNthReturnedWith(1, 'Lemon')
10
expect(drink).toHaveNthReturnedWith(2, 'Orange')
11
})

2.4 ๋ฐฐ์—ด ๋ฐ ๊ฐ์ฒด Matcher

  • toContain(item) : ๋ฐฐ์—ด ๋˜๋Š” ๋ฌธ์ž์—ด์— ํŠน์ • ํ•ญ๋ชฉ์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toContainEqual(item) : ๋ฐฐ์—ด์— ๊ตฌ์กฐ์ ์œผ๋กœ ๊ฐ™์€ ํ•ญ๋ชฉ์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveLength(number) : ๋ฐฐ์—ด, ๋ฌธ์ž์—ด ๋˜๋Š” ๊ฐ์ฒด์˜ ๊ธธ์ด/ํฌ๊ธฐ๊ฐ€ ํŠน์ • ๊ฐ’๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toHaveProperty(keyPath, value?) : ๊ฐ์ฒด์— ํŠน์ • ๊ฒฝ๋กœ์˜ ์†์„ฑ์ด ์กด์žฌํ•˜๊ณ , ์„ ํƒ์ ์œผ๋กœ ํ•ด๋‹น ์†์„ฑ์˜ ๊ฐ’์ด ํŠน์ • ๊ฐ’๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • toMatchObject(object) : ๊ฐ์ฒด๊ฐ€ ํŠน์ • ๊ฐ์ฒด์™€ ๋ถ€๋ถ„์ ์œผ๋กœ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค

2.4.1 toContainEqual()

1
describe('my beverage', () => {
2
test('is delicious and not sour', () => {
3
const myBeverage = { delicious: true, sour: false }
4
expect([myBeverage, ...]).toContainEqual(myBeverage)
5
})
6
})

2.4.2 toHaveProperty()

1
const houseForSale = {
2
bath: true,
3
bedrooms: 4,
4
kitchen: {
5
amenities: ['oven', 'stove', 'washer'],
6
area: 20,
7
wallColor: 'white',
8
'nice.oven': true,
9
},
10
liveingroomm: {
11
amenities: [
12
{
13
couch: [
14
['large', { dimensions: [20, 20] }],
15
['small', { dimensions: [10, 10] }],
16
],
17
},
18
],
19
},
20
'ceiling.height': 2,
21
}
1
test('this house has my desired features', () => {
2
expect(houseForSale).toHaveProperty('bath')
3
expect(houseForSale).toHaveProperty('bedrooms', 4)
4
5
expect(houseForSale).not.toHaveProperty('pool')
6
7
// '.'๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊นŠ๊ฒŒ ๋ ˆํผ๋Ÿฐ์‹ฑํ•˜๊ธฐ
8
expect(houseForSale).toHaveProperty('kitchen.area', 20)
9
expect(houseForSale).toHaveProperty('kitchen.amenities', ['oven', 'stove', 'washer'])
10
11
expect(houseForSale).not.toHaveProperty('kitchen.open')
12
13
// Array๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊นŠ๊ฒŒ ๋ ˆํผ๋Ÿฐ์‹ฑํ•˜๊ธฐ
14
expect(houseForSale).toHaveProperty(['kitchen', 'area'], 20)
15
expect(houseForSale).toHaveProperty(
16
['kitchen', 'amenities'], //
17
['oven', 'stove', 'washer'],
18
)
19
expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], 'oven')
20
expect(houseForSale).toHaveProperty(
21
'liveingroomm.amenities[0].couch[0][1].dimensions[0]', //
22
20,
23
)
24
expect(houseForSale).toHaveProperty(['kitchen', 'nice.oven'])
25
expect(houseForSale).not.toHaveProperty(['kitchen', 'open'])
26
27
// ํ‚ค๊ฐ’ ์ž์ฒด์— '.'์ด ์žˆ๋Š” ๊ฒฝ์šฐ Array ์‚ฌ์šฉ
28
expect(houseForSale).toHaveProperty(['ceiling.height'], 'tall')
29
})

2.5 ์—๋Ÿฌ Matcher

  • toThrow(error?) : ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ ํŠน์ • ์˜ค๋ฅ˜๋ฅผ ๋˜์ง€๋Š”์ง€ ํ™•์ธํ•œ๋‹ค

2.5.1 toThrow()

1
function drinkFlavor(flavor) {
2
if (flavor === 'octopus') {
3
throw new DisgustingFlavorError('์œผ์•… ๋ฌธ์–ด ๋…ธ๋ง›์ด์•ผ')
4
}
5
}
1
test('throws on octopus', () => {
2
function drinkOctopus() {
3
drinkFlavor('octopus')
4
}
5
6
// ์—๋Ÿฌ๋ฉ”์‹œ์ง€ ์–ด๋”˜๊ฐ€์— '๋…ธ๋ง›'์ด๋ผ๊ณ  ์จ์ ธ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
7
expect(drinkOctopus).toThrow(/๋…ธ๋ง›/)
8
expect(drinkOctopus).toThrow('๋…ธ๋ง›')
9
10
// ์ •ํ™•ํ•œ ๋ฌธ์žฅ์„ ํ…Œ์ŠคํŠธํ•œ๋‹ค.
11
expect(drinkOctopus).toThrow(/^์œผ์•… ๋ฌธ์–ด ๋…ธ๋ง›์ด์•ผ!$/)
12
expect(drinkOctopus).toThrow(new Error('์œผ์•… ๋ฌธ์–ด ๋…ธ๋ง›์ด์•ผ!'))
13
14
// DisgustingFlavorError ํƒ€์ž…์˜ ์—๋Ÿฌ๊ฐ€ ๋˜์ ธ์ง€๋Š”๊ฑธ ํ™•์ธํ•œ๋‹ค.
15
expect(drinkOctopus).toThrow(DisgustingFlavorError)
16
})

2.6 ๊ธฐํƒ€ Matcher

  • toStrictEqual(value): ๊ฐ์ฒด๊ฐ€ ๊ตฌ์กฐ์ ์œผ๋กœ ์™„๋ฒฝํžˆ ๋™์ผํ•œ์ง€ ํ™•์ธํ•œ๋‹ค (ํ”„๋กœํ† ํƒ€์ž… ๋ฐ ๋น„์—ด๊ฑฐํ˜• ์†์„ฑ ํฌํ•จ).
  • toBeInstanceOf(Class): ๊ฐ’์ด ํŠน์ • ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค์ธ์ง€ ํ™•์ธํ•œ๋‹ค.
  • toMatch(regexp | string): ๋ฌธ์ž์—ด์ด ์ •๊ทœ ํ‘œํ˜„์‹ ๋˜๋Š” ๋ฌธ์ž์—ด๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • expect.anything(): ์•„๋ฌด ๊ฐ’์ด๋‚˜ ํ—ˆ์šฉํ•˜์ง€๋งŒ null์ด๋‚˜ undefined๋Š” ์ œ์™ธํ•œ๋‹ค.
  • expect.any(constructor): ํŠน์ • ์ƒ์„ฑ์ž์˜ ์ธ์Šคํ„ด์Šค์ธ์ง€ ํ™•์ธํ•œ๋‹ค.
  • expect.arrayContaining(array): ์ž…๋ ฅ๋œ array๊ฐ€ ๋น„๊ต ๋Œ€์ƒ array์˜ subset์ธ์ง€ ํ™•์ธํ•œ๋‹ค. (์ „๋ถ€ ํฌํ•จํ•˜๋Š”์ง€)
  • expect.objectContaining(object): ์ž…๋ ฅ๋œ ๊ฐ์ฒด๊ฐ€ ๋น„๊ต ๋Œ€์ƒ ๊ฐ์ฒด์˜ subset์ธ์ง€ ํ™•์ธํ•œ๋‹ค. (์ „๋ถ€ ํฌํ•จํ•˜๋Š”์ง€)
  • expect.stringContaining(string): ํŠน์ • ๋ฌธ์ž์—ด์ด ํฌํ•จ ๋ผ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค

2.6.1 arrayContaining()

1
describe('arrayContaining', () => {
2
const expected = ['Alice', 'Bob']
3
4
it('matches even if received contains additional elements', () => {
5
expect(['Alice', 'Bob', 'Eve']).toEqual(expect.arrayContaining(expected))
6
})
7
})

3. Modifiers

  • not : ๋ฐ˜๋Œ€ ํ…Œ์ŠคํŠธ๋กœ ์ „ํ™˜
  • resolves : Promise ์ •์ƒ ๋ฐ˜ํ™˜์œผ๋กœ ์ „ํ™˜
  • rejects : Promise ๋˜์ง€๋Š” ์ƒํ™ฉ์œผ๋กœ ์ „ํ™˜

3.1 not

1
test('flavor is not coconut', () => {
2
expect('apple').not.toBe('coconut')
3
})

3.2 resolve

1
test('resolves to lemon', () => {
2
// make sure to add a return statement
3
return expect(Promise.resolve('lemon')).resolves.toBe('lemon')
4
})

3.3 reject

1
test('rejects to octopus', () => {
2
return expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus')
3
})

4. Mock / Stub / Fake

ํ…Œ์ŠคํŠธํ• ๋•Œ ์˜์กด์„ฑ(Dependency)๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋‹ค์–‘ํ•˜๊ฒŒ ์กด์žฌํ•œ๋‹ค. ๋ชจ๋“  ์˜์กด์„ฑ (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋“ฑ)์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋Š” ํ…Œ์ŠคํŠธ๋„ ์กด์žฌํ•˜์ง€ ๋งŒ ๊ทธ๋Ÿฐ ํ…Œ์ŠคํŠธ๋Š” ๋„ˆ๋ฌด ๋ฌด๊ฒ๊ณ  ์˜ค๋ž˜๊ฑธ๋ฆฐ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ๋””ํŽœ๋˜์‹œ๋ฅผ ๊ฐ์ž ๊ฐ์ฒด๋กœ ์Šค์™‘ ํ›„ ์‚ฌ์šฉํ•œ๋‹ค.

  • Mock : Mock์€ ์ƒํ˜ธ์ž‘์šฉ์„ ๊ฒ€์ฆํ•˜๋Š” ๊ฐ์ฒด์ด๋‹ค.
  • Stub : Stub์€ ํ•จ์ˆ˜๋‚˜ ๊ฐ์ฒด์˜ ๊ฐ„์†Œํ™”๋œ ๋ฒ„์ „์œผ๋กœ ๋ฏธ๋ฆฌ ์ •์˜๋œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • Fake : Fake๋Š” ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ๊ฐ„์†Œํ•˜๊ฒŒ ๊ตฌํ˜„ํ•œ ํ˜•ํƒœ์ด๋‹ค.
    • ๋ณต์žกํ•œ ์‹ค์ œ ๊ฐ์ฒด์˜ ์ž‘๋™ ๋ฐฉ์‹์„ ์ตœ์†Œํ™”ํ•˜์—ฌ ๊ตฌํ˜„ํ•œ ํ˜•ํƒœ์ด๋‹ค.
    • ์‹ค์ œ ๊ฐ์ฒด๋Š” ๋„ˆ๋ฌด ํ—ค๋น„ํ•˜์ง€๋งŒ Stub ๋ณด๋‹ค๋Š” ํ˜„์‹ค์ ์ธ ์ž‘๋™์ด ํ•„์š”ํ• ๋•Œ ๋งŽ์ด ์‚ฌ์šฉ๋œ๋‹ค.

์˜์กด์„ฑ ํ•ด๊ฒฐ์„ ํ•ด์ฃผ๋Š” ๊ฐ์ฒด๊ฐ€ ์…‹์ค‘ ๊ผญ ์–ด๋А ํ•˜๋‚˜์— ์†ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•  ํ•„์š”๋Š” ์—†๋‹ค. Mock์ด๋ฉด์„œ Stub์ผ ์ˆ˜ ์žˆ๋‹ค. ๋ช…์นญ์€ ์œ„์™€ ๊ฐ™์ด ์ •์˜ํ•˜์ง€๋งŒ ์ผ๋ฐ˜ ์ ์œผ๋ก  ์ผ๊ด„์ ์œผ๋กœ Mock์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.


4.1 Mock

1
import { Test, TestingModule } from '@nestjs/testing'
2
import { UserService } from './user.service'
3
import { UserRepository } from './user.repository'
4
5
describe('UserService with Mock', () => {
6
let userService: UserService
7
let userRepositoryMock: { findById: jest.Mock }
8
9
beforeEach(async () => {
10
userRepositoryMock = { findById: jest.fn() } // Mock ์ƒ์„ฑํ•˜๊ธฐ
11
12
const module: TestingModule = await Test.createTestingModule({
13
providers: [
14
UserService,
15
{
16
provide: UserRepository,
17
useValue: userRepositoryMock, // Mock ์ฃผ์ž…ํ•˜๊ธฐ
18
},
19
],
20
}).compile()
21
22
userService = module.get<UserService>(UserService)
23
})
24
25
it('should call findById on UserRepository', () => {
26
const userId = '1'
27
userService.findUserById(userId)
28
29
// ์‹คํ–‰๋œ๊ฑธ ํ™•์ธ
30
expect(userRepositoryMock.findById).toHaveBeenCalledWith(userId)
31
32
// ํ•œ๋ฒˆ๋งŒ ๋ถˆ๋ฆฐ๊ฑธ ํ™•์ธ
33
expect(userRepositoryMock.findById).toHaveBeenCalledTimes(1)
34
})
35
})

4.2 Stub

1
import { Test, TestingModule } from '@nestjs/testing'
2
import { UserService } from './user.service'
3
import { UserRepository } from './user.repository'
4
5
describe('UserService with Stub', () => {
6
let userService: UserService
7
8
beforeEach(async () => {
9
const userRepositoryStub = {
10
findById: (id: string) => ({ id, name: 'Stubbed User' }), // Stub ์ƒ์„ฑํ•˜๊ธฐ
11
}
12
13
const module: TestingModule = await Test.createTestingModule({
14
providers: [
15
UserService,
16
{
17
provide: UserRepository,
18
useValue: userRepositoryStub, // Stub ์ฃผ์ž…ํ•˜๊ธฐ
19
},
20
],
21
}).compile()
22
23
userService = module.get<UserService>(UserService)
24
})
25
26
it('should return the stubbed user', () => {
27
const userId = '1'
28
const result = userService.findUserById(userId)
29
30
// ๋ณ€ํ™˜๊ฐ’ ๊ฒ€์ฆ
31
expect(result).toEqual({ id: userId, name: 'Stubbed User' })
32
})
33
})

4.3 Fake

1
import { Test, TestingModule } from '@nestjs/testing'
2
import { UserService } from './user.service'
3
import { UserRepository } from './user.repository'
4
5
// Faks ์ƒ์„ฑ
6
class FakeUserRepository {
7
private user = [{ id: '1', name: 'Fake User' }]
8
9
findById(id: string) {
10
return this.users.find(user => user.id === id) || null
11
}
12
}
13
14
describe('UserService with Fake', () => {
15
let userService: UserService
16
17
beforeEach(async () => {
18
const module: TestingModule = await Test.createTestingModule({
19
providers: [
20
UserService,
21
{
22
provide: UserRepository,
23
useClass: FakeUserRepository, // Fake ์ฃผ์ž…ํ•˜๊ธฐ
24
},
25
],
26
}).compile()
27
28
userService = module.get<UserService>(UserService)
29
})
30
31
it('should return the fake user', () => {
32
const userId = '1'
33
const result = userService.findUserById(userId)
34
35
// ๊ฒฐ๊ณผ๊ฐ’ ๊ฒ€์ฆ
36
expect(result).toEqual({ id: userId, name: 'Fake User' })
37
})
38
39
it('should return null if user not found', () => {
40
const userId = '2'
41
const result = userService.findUserById(userId)
42
43
// ๊ฒฐ๊ณผ๊ฐ’ ๊ฒ€์ฆ
44
expect(result).toBeNull()
45
})
46
})

4.4 Mock vs Stub vs Fake

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

4.5 Mock Function

1
import { Test, TestingModule } from '@nestjs/testing'
2
import { UserService } from './user.service'
3
import { UserRepository } from './user.repository'
4
5
describe('UserService with Mock', () => {
6
let userService: UserService
7
let userRepositoryMock: { findById: jest.Mock }
8
9
beforeEach(async () => {
10
userRepositoryMock = { findById: jest.fn() } // Mock ์ƒ์„ฑํ•˜๊ธฐ
11
12
const module: TestingModule = await Test.createTestingModule({
13
providers: [
14
UserService,
15
{
16
provide: UserRepository,
17
useValue: userRepositoryMock, // Mock ์ฃผ์ž…ํ•˜๊ธฐ
18
},
19
],
20
}).compile()
21
22
userService = module.get<UserService>(UserService)
23
})
24
25
it('should call findById on UserRepository', () => {
26
const userId = '1'
27
userService.findUserById(userId)
28
29
// ์‹คํ–‰๋œ๊ฑธ ํ™•์ธ
30
expect(userRepositoryMock.findById).toHaveBeenCalledWith(userId)
31
32
// ํ•œ๋ฒˆ๋งŒ ๋ถˆ๋ฆฐ๊ฑธ ํ™•์ธ
33
expect(userRepositoryMock.findById).toHaveBeenCalledTimes(1)
34
})
35
})

4.6 Mock Function ์†์„ฑ ์ ‘๊ทผ์ž

  • mockFn.mock.calls: mock function์ด ํ˜ธ์ถœ๋œ ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค์˜ ๋ฐฐ์—ด์„ ํฌํ•จํ•œ๋‹ค.
  • mockFn.mock.results: mock function์˜ ๊ฐ ํ˜ธ์ถœ์ด ๋ฐ˜ํ™˜ํ•œ ๊ฐ’ ๋˜๋Š” ์˜ˆ์™ธ๋ฅผ ํฌํ•จํ•˜๋Š” ๊ฐ์ฒด์˜ ๋ฐฐ์—ด์ด๋‹ค.
  • mockFn.mock.instances: mock function์ด ํ˜ธ์ถœ๋  ๋•Œ๋งˆ๋‹ค ์ƒ์„ฑ๋œ this ์ธ์Šคํ„ด์Šค๋ฅผ ํฌํ•จํ•˜๋Š” ๋ฐฐ์—ด์ด๋‹ค

4.7 mock.instances

1
const mockFn = jest.fn()
2
3
const a = new mockFn()
4
const b = new mockFn()
5
6
mockFn.mock.instances[0] === a // true
7
mockFn.mock.instances[1] === b // true

4.8 Mock Function ๊ตฌํ˜„

  • mockFn.mockImplementation(fn) : mock function์˜ ๊ตฌํ˜„์ฒด๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค. (์‹คํ–‰ํ•  ํ•จ์ˆ˜)
  • mockFn.mockImplementationOnce(fn) : mockImplementation์„ ๋‹จ ํ•œ๋ฒˆ๋งŒ ์‹คํ–‰ํ•œ๋‹ค.
    • ์—ฌ๋Ÿฌ๋ฒˆ chaining ๊ฐ€๋Šฅํ•˜๋‹ค.
  • mockFn.mockReturnThis() : mock function์ด ํ˜ธ์ถœ๋  ๋•Œ๋งˆ๋‹ค this๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ•œ๋‹ค.
  • mockFn.mockReturnValue(value) : mock function์ด ํ˜ธ์ถœ๋  ๋•Œ๋งˆ๋‹ค ํŠน์ • ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ•œ๋‹ค.
  • mockFn.mockReturnValueOnce(value) : mockReturnValue๋ฅผ ๋‹จ ํ•œ๋ฒˆ๋งŒ ์‹คํ–‰ํ•œ๋‹ค.
    • ์—ฌ๋Ÿฌ๋ฒˆ chaining ๊ฐ€๋Šฅํ•˜๋‹ค.
  • mockFn.mockResolvedValue(value) : mock function์ด ํ˜ธ์ถœ๋  ๋•Œ Promise๊ฐ€ ํŠน์ • ๊ฐ’์œผ๋กœ Resolve ๋˜๋„๋ก ํ•œ๋‹ค.
  • mockFn.mockResolvedValueOnce(value) : mockResolvedValue๋ฅผ ๋‹จ ํ•œ๋ฒˆ๋งŒ ์‹คํ–‰ํ•œ๋‹ค.
    • ์—ฌ๋Ÿฌ๋ฒˆ chaining ๊ฐ€๋Šฅํ•˜๋‹ค.
  • mockFn.mockRejectedValue(value) : mock function์ด ํ˜ธ์ถœ๋  ๋•Œ Promise๊ฐ€ ํŠน์ • ๊ฐ’์œผ๋กœ Reject ๋˜๋„๋ก ์„ค์ •ํ•œ๋‹ค.
  • mockFn.mockRejectedValueOnce(value) : mock function์˜ ๋‹ค์Œ ํ•œ ๋ฒˆ์˜ ํ˜ธ์ถœ์— ๋Œ€ํ•ด ํ”„๋กœ๋ฏธ์Šค๊ฐ€ ํŠน์ • ๊ฐ’์œผ๋กœ ๊ฑฐ๋ถ€๋˜๋„๋ก ์„ค์ •ํ•œ๋‹ค

4.9 mockImplementation()

1
const mockFn = jest.fn(scalar => 42 + scalar)
2
3
mockFn(0) // 42
4
mockFn(1) // 43
5
6
mockFn.mockImplementation(scalar => 36 + scalar)
7
8
mockFn(2) // 38
9
mockFn(3) // 39

4.10 mockReturnThis()

1
jest.fn(function () {
2
return this
3
})

4.11 mockReturnValue()

1
jest.fn().mockImplementation(() => value)

4.12 mockResolvedValue()

1
jest.fn().mockImplementation(() => Promise.resolve(value))

4.13 mockRejectedValue()

1
jest.fn().mockImplementation(() => Promise.reject(value))

4.14 Mock Function ๊ตฌํ˜„

  • mockFn.mockClear(): mock function์˜ ํ˜ธ์ถœ ๊ธฐ๋ก๊ณผ ๋ฐ˜ํ™˜ ๊ฐ’๋“ค์„ ์ง€์šด๋‹ค (์ƒํƒœ ์ดˆ๊ธฐํ™”).
  • mockFn.mockReset(): mockClear()์˜ ๊ธฐ๋Šฅ์„ ๋ชจ๋‘ ์‹คํ–‰ํ•˜๊ณ  mock ํ•จ์ˆ˜๋ฅผ ๋นˆ ํ•จ์ˆ˜๋กœ ๋Œ€์ฒดํ•œ๋‹ค.
  • mockFn.mockRestore(): mockReset()์˜ ์ž‘์—…์„ ๋ชจ๋‘ ์ง„ํ–‰ํ•˜๊ณ  mock ํ•จ์ˆ˜๋ฅผ ์›๋ž˜ ๊ตฌํ˜„์ฒด๋กœ ๋ณต์›ํ•œ๋‹ค

5. ํ…Œ์ŠคํŒ…์˜ ์ข…๋ฅ˜

test-1_1

  • Unit Testing : ํ•จ์ˆ˜๋‚˜ ํด๋ž˜์Šค์ฒ˜๋Ÿผ ๊ฐ€์žฅ ์ž‘์€ ๋‹จ์œ„์˜ ๋กœ์ง์„ โ€˜๋…๋ฆฝ์ ์œผ๋กœโ€™ ํ…Œ์ŠคํŠธํ•œ๋‹ค
  • Integration Testing : ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋“ฑ ๋‹ค์–‘ํ•œ ์„œ๋น„์Šค์˜ ์š”์†Œ๋“ค์„ ํ•จ๊ป˜ ์‹คํ–‰ ํ–ˆ์„๋•Œ ๋ฌธ์ œ๊ฐ€ ์—†๋Š”์ง€ ํ™•์ธํ•œ๋‹ค
  • End to End Testing : ์‚ฌ์šฉ์ž์˜ ๊ด€์ ์—์„œ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉ ํ–ˆ์„๋•Œ ํ”„๋กœ๊ทธ๋žจ์ด ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค

5.1 NestJS Testing ์˜ˆ์ œ

1
import { CatsController } from './cats.controller'
2
import { CatsService } from './cats.service'
3
4
describe('CatsController', () => {
5
let catsController: CatsController
6
let catsService: CatsService
7
8
beforeEach(() => {
9
catsService = new CatsService()
10
catsController = new CatsController(catsService)
11
})
12
13
describe('findAll', () => {
14
it('should return an array of cats', async () => {
15
const result = ['test']
16
jest.spyOn(catsService, 'findAll').mockImplementation(() => result)
17
18
expect(await catsController.findAll()).toBe(result)
19
})
20
})
21
})
1
import { Test } from '@nestjs/testing'
2
import { CatsController } from './cats.controller'
3
import { CatsService } from './cats.service'
4
5
describe('CatsController', () => {
6
let catsController: CatsController
7
let catsService: CatsService
8
9
beforeEach(async () => {
10
const module = await Test.createTestingModule({
11
controllers: [CatsController],
12
providers: [CatsService],
13
}).compile()
14
15
catsService = modulRef.get<CatsService>(CatsService)
16
catsController = moduleRef.get<CatsController>(CatsController)
17
})
18
19
describe('findAll', () => {
20
it('should return an array of cats', async () => {
21
const result = ['test']
22
jest.spyOn(catsService, 'findAll').mockImplementation(() => result)
23
24
expect(await catsController.findAll()).toBe(result)
25
})
26
})
27
})

5.2 Unit Testing ์˜ˆ์ œ

1
@Controller('uesrs')
2
export class UsersController {
3
constructor(private readonly usersService: UsersService) {}
4
5
@Get(':id')
6
async getUserById(@Param('id', ParseIntPipe) id: number): Promise<User> {
7
return this.usersService.findUserById(id)
8
}
9
}
1
@Injectable()
2
export class UsersService {
3
constructor(
4
@InjectRepository(User)
5
private readonly userRepository: Repository<User>,
6
) {}
7
8
async findUserById(id: number): Promise<User> {
9
if (id === '2') return null
10
11
return await this.userRepository.findOne({
12
where: { id },
13
})
14
}
15
}
1
describe('UserController', () => {
2
let usersController: UsersController
3
let usersService: UsersService
4
5
beforeEach(async () => {
6
const module: TestingModule = await Test.createTestingModule({
7
controllers: [UsersController],
8
providers: [
9
{
10
provide: UsersService,
11
useValue: {
12
findUserById: jest.fn(),
13
},
14
},
15
],
16
}).compile()
17
18
usersService = module.get<UsersService>(UsersService)
19
usersController = module.get<UsersController>(UsersController)
20
})
21
22
it('should call UserService.findUserById with the correct parameter', async () => {
23
const id = '1'
24
const userServiceSpy = jest
25
.spyOn(usersService, 'findUserById') //
26
.mockResolvedValueOnce({ id, name: 'John Doe' })
27
const result = userController.getUserById(id)
28
29
expect(userServiceSpy).toHaveBeenCalledWith(id)
30
expect(result).toEqual({ id, name: 'John Doe' })
31
})
32
})

5.3 Integration Testing ์˜ˆ์ œ

1
@Controller('users')
2
export class UsersController {
3
constructor(private readonly usersService: UsersService) {}
4
5
@Get(':id')
6
async getUserById(@Param('id', ParseIntPipe) id: number): Promise<User> {
7
return this.usersService.findUserById(id)
8
}
9
}
1
@Injectable()
2
export class UsersService {
3
constructor(
4
@InjectRepository(User)
5
private readonly userRepository: Repository<User>,
6
) {}
7
8
async findUserById(id: number): Promise<User> {
9
if (id === '2') return null
10
11
return await this.userRepository.findOne({
12
where: { id },
13
})
14
}
15
}
1
describe('UserController (Integration)', () => {
2
let userController: UsersController
3
let usersService: UsersService
4
5
beforeEach(async () => {
6
const module: TestingModule = await Test.createTestingModule({
7
controllers: [UsersController],
8
providers: [UsersService],
9
}).compile()
10
11
userController = module.get<UsersController>(UsersController)
12
usersService = module.get<UsersService>(UsersService)
13
})
14
15
it('should return user data for a valid ID', () => {
16
const id = '1'
17
const result = userController.getUserById(id)
18
expect(result).toEqual({ id: '1', name: 'John Doe' })
19
})
20
21
it('should return null for an invalid ID', () => {
22
const id = '2'
23
const result = userController.getUserById(id)
24
expect(result).toBeNull()
25
})
26
})

5.4 End to End Testing ์˜ˆ์ œ

1
describe('UserController (E2E)', () => {
2
let app: INestApplication
3
4
beforeAll(async () => {
5
const moduleFixture: TestingModule = await Test.createTestingModule({
6
imports: [AppModule],
7
}).compile()
8
9
app = moduleFixture.createNestApplication()
10
await app.init()
11
})
12
13
it('/users/:id (GET) - should return user data for a valid ID', async () => {
14
const id = '1'
15
const response = await request(app.getHttpServer()).get(`/users/${id}`).expect(200)
16
17
expect(response.body).toEqual({ id: '1', name: 'John Doe' })
18
})
19
20
it('/users/:id (GET) - should return 404 for an invalid ID', async () => {
21
const id = '2'
22
await request(app.getHttpServer()).get(`/users/${id}`).expect(404)
23
})
24
25
afterAll(async () => {
26
await app.close()
27
})
28
})

6. Coverage

์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ์†Œํ”„ํŠธ์›จ์–ด ํ…Œ์ŠคํŠธ์—์„œ ํ…Œ์ŠคํŠธ๊ฐ€ ์†Œ์Šค ์ฝ”๋“œ์˜ ์–ด๋А ๋ถ€๋ถ„์„ ์–ผ๋งˆ๋‚˜ ์‹คํ–‰ํ–ˆ๋Š”์ง€๋ฅผ ์ธก์ •ํ•˜๋Š” ์ง€ํ‘œ๋‹ค. ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ํ…Œ์ŠคํŠธ์˜ ํšจ๊ณผ์„ฑ์„ ํ‰๊ฐ€ํ•˜๊ณ , ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์€ ์ฝ”๋“œ ์˜์—ญ์„ ์‹๋ณ„ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ํ’ˆ์งˆ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋ฐ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•œ๋‹ค.

์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€์˜ ์œ ํ˜•

  • ๋ผ์ธ ์ปค๋ฒ„๋ฆฌ์ง€(Line Coverage)
    • ์ฝ”๋“œ์˜ ๊ฐ ๋ผ์ธ์ด ํ…Œ์ŠคํŠธ์— ์˜ํ•ด ์‹คํ–‰๋˜์—ˆ๋Š”์ง€๋ฅผ ์ธก์ •ํ•œ๋‹ค.
    • e.g. ์ „์ฒด ์ฝ”๋“œ ๋ผ์ธ ์ค‘ ๋ช‡ ํผ์„ผํŠธ๊ฐ€ ํ…Œ์ŠคํŠธ๋˜์—ˆ๋Š”์ง€.
  • ๋ถ„๊ธฐ ์ปค๋ฒ„๋ฆฌ์ง€(Branch Coverage)
    • ์กฐ๊ฑด๋ฌธ(if, switch ๋“ฑ)์˜ ๊ฐ ๋ถ„๊ธฐ๊ฐ€ ํ…Œ์ŠคํŠธ์— ์˜ํ•ด ์‹คํ–‰๋˜์—ˆ๋Š”์ง€๋ฅผ ์ธก์ •ํ•œ๋‹ค.
    • e.g. if-else ๋ฌธ์—์„œ if์™€ else ๊ฐ๊ฐ์ด ํ…Œ์ŠคํŠธ๋˜์—ˆ๋Š”์ง€.
  • ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€(Function Coverage)
    • ์ฝ”๋“œ์˜ ๊ฐ ํ•จ์ˆ˜๊ฐ€ ํ…Œ์ŠคํŠธ์— ์˜ํ•ด ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€๋ฅผ ์ธก์ •ํ•œ๋‹ค.
    • e.g. ์ „์ฒด ํ•จ์ˆ˜ ์ค‘ ๋ช‡ ํผ์„ผํŠธ๊ฐ€ ํ…Œ์ŠคํŠธ๋˜์—ˆ๋Š”์ง€.
  • ์กฐ๊ฑด ์ปค๋ฒ„๋ฆฌ์ง€(Condition Coverage)
    • ์กฐ๊ฑด๋ฌธ ๋‚ด์˜ ๊ฐ ๊ฐœ๋ณ„ ์กฐ๊ฑด์ด ํ…Œ์ŠคํŠธ์— ์˜ํ•ด ์ฐธ๊ณผ ๊ฑฐ์ง“์œผ๋กœ ํ‰๊ฐ€๋˜์—ˆ๋Š”์ง€๋ฅผ ์ธก์ •ํ•œ๋‹ค.
    • e.g. ๋ณตํ•ฉ ์กฐ๊ฑด๋ฌธ์—์„œ ๊ฐ ์กฐ๊ฑด์ด ์ฐธ๊ณผ ๊ฑฐ์ง“์œผ๋กœ ํ…Œ์ŠคํŠธ๋˜์—ˆ๋Š”์ง€

6.1 Coverage (Statement)

1
// Statement
2
const sum = (a, b) => a + b
3
// Statement
4
console.log(sum(1, 2))

6.2 Coverage (Branch)

1
if (x > 10) {
2
// ํ•˜๋‚˜์˜ ๋ถ„๊ธฐ
3
console.log('x๋Š” 10๋ณด๋‹ค ํฝ๋‹ˆ๋‹ค')
4
} else {
5
// ํ•˜๋‚˜์˜ ๋ถ„๊ธฐ
6
console.log('x๋Š” 10 ์ดํ•˜์ž…๋‹ˆ๋‹ค')
7
}

6.3 Coverage (Function)

1
function add(a, b) {
2
// ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
3
return a + b
4
}
5
6
function subtract(a, b) {
7
// ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
8
return a - b
9
}

6.4 Coverage (Line)

1
const multiply = (a, b) => {
2
// ๋ผ์ธ์ž…๋‹ˆ๋‹ค.
3
return a * b
4
}
5
6
// ๋ผ์ธ
7
console.log(multiply(2, 3))

6.5 ํ…Œ์ŠคํŠธ ํ•˜์ง€ ์•Š๋Š”๊ฒƒ๋“ค

  • ํ”„๋ ˆ์ž„์›Œํฌ ๊ธฐ๋Šฅ
    • ๊ทผ๋ณธ์ ์œผ๋กœ ํ”„๋ ˆ์ž„์›Œํฌ ์ž์ฒด์ ์œผ๋กœ ์œ ๋‹› ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์„๊ฑฐ๋ž€ ๊ฐ€์ •์„ ํ•œ๋‹ค.
    • e.g. NestJS์˜ UseGuard Annotation์ด ์ž˜ ์ž‘๋™ํ•˜๋Š” ์ง€ ํ…Œ์ŠคํŠธ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
    • NestJS ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๋์„๊ฑฐ๋ผ๊ณ  ๊ฐ€์ •ํ•œ๋‹ค. ๊ทธ๋Ÿผ์—๋„ ์ •๋ง ํ•˜๊ณ ์‹ถ๋‹ค๋ฉด ์ ˆ๋Œ€๋กœ ํ•˜๋ฉด ์•ˆ๋˜๋Š”๊ฑด ์•„๋‹ˆ๋‹ค.
  • ์™ธ๋ถ€ ๋””ํŽœ๋˜์‹œ
    • ๋‚ฎ์€ ์ˆ˜์ค€์˜ ํ…Œ์ŠคํŠธ์ผ์ˆ˜๋ก (Unit Test, Integration Test) TypeORM, Logger๋“ฑ ์™ธ๋ถ€ ๋””ํŽœ๋˜์‹œ๊ฐ€ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
    • ๋Œ€์‹  Mock, Stub, Fake๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ธฐ๋Šฅ์„ ๋ชจ๋ฐฉํ•˜๊ณ  ์‹ค์ œ ๋‚ด ์ฝ”๋“œ์ƒ์˜ ์ค‘์š”ํ•œ ๋กœ์ง์„ ํ…Œ์ŠคํŠธํ•œ๋‹ค.
    • ๊ทผ๋ณธ์ ์œผ๋กœ ๋‚ด ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ฉด ํ…Œ์Šค ํŠธ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • ํผํฌ๋จผ์Šค
    • ํผํฌ๋จผ์Šค ํ…Œ์ŠคํŠธ๋Š” ๋ณดํ†ต ๋‹ค๋ฅธ ๋กœ๋“œํ…Œ์ŠคํŠธ ํˆด์„ ์‚ฌ์šฉํ•ด์„œ ์ง„ํ–‰ํ•œ๋‹ค.
    • Unit Test, Integration Test, End to End Test ๋“ฑ์€ ๊ทผ๋ณธ์ ์œผ๋กœ ๋กœ์ง์˜ ์ •์ƒ ์ž‘๋™ ์—ฌ๋ถ€๋ฅผ ํ…Œ์ŠคํŠธํ•œ๋‹ค.
    • ํผํฌ๋จผ์Šค์™€ ๋กœ๋“œ ํ…Œ์ŠคํŠธ๋Š” ๋”ฐ๋กœ ์ง„ํ–‰ํ•˜๋„๋ก ํ•œ๋‹ค.
  • ๋กœ์ง์ด ์—†๋Š” ์ฝ”๋“œ
    • ์ดˆ๋ณด์ž๋“ค์ด coverage๋ฅผ ์˜ฌ๋ฆฌ๊ธฐ ์œ„ํ•ด์„œ ํ”ํžˆ ํ•˜๋Š” ์‹ค์ˆ˜๋‹ค.
    • NestJS๋ฅผ ์˜ˆ๋ฅผ๋“ค๋ฉด Dto๋‚˜ Entity๋ฅผ ํ…Œ์ŠคํŠธ ํ•  ํ•„์š” ์—†๋‹ค. ๊ทธ๋ƒฅ ignore ๋ฆฌ์ŠคํŠธ์— ๋„ฃ์–ด๋ฒ„๋ฆฌ์ž