1. ํ ์คํ
ํ
์คํ
์ ์ํํธ์จ์ด๊ฐ ์์ํ๋๋ก ์คํ๋๋์ง ๊ฒ์ฆํ๊ณ ํ์ธํ๋ ๊ณผ์ ์ด๋ค
ํ ์คํ ์ ์ค์์ฑ
QA(Quality Assurance): ๋ฏธ๋ฆฌ ๋ฒ๊ทธ๋ฅผ ๋ฐ๊ฒฌํด์ ํ๋ก๋์ ํ๊ฒฝ์์ ๋ฌธ์ ๊ฐ ์๊ธฐ๋๊ฑธ ๋ฐฉ์งํ๋ค๋์ฑ ๋น ๋ฅธ ๋ฆฌํฉํฐ๋ง: ๋ณ๊ฒฝํ ์ฝ๋๊ฐ ๊ธฐ์กด ๋ก์ง์์ ๋ฌธ์ ๋ฅผ ์ผ์ผํฌ๊ฒฝ์ฐ ๋ฏธ๋ฆฌ ์ ์ ์๋ค๋คํ๋ฉํ ์ด์ ์ญํ: ํ ์คํธ ์์ฒด๊ฐ ์ฝ๋๊ฐ ์ด๋ป๊ฒ ์๋ํด์ผํ๋์ง ์ค๋ช ํ๋ ํ๋์ ๋ฌธ์ ์ญํ ์ ํ๋ค์ฝ๋ ๋ก์ง ๊ฒ์ฆ ์๋ํ: ํ ์คํธ ์ฝ๋๋ฅผ ์คํํด์ ๋ก์ง ๊ฒ์ฆ์ ์๋ํํ์ฌ ์ง์ ํ๋ก๊ทธ๋จ์ ํ ์คํธํ ํ์๊ฐ ์์ด์ง๋ค
1.1 Testing์ ์ค์์ฑ
1import { Injectable } from '@nestjs/common'23@Injectable()4export class MathServerice {5add(a: number, b: number): number {6return a + b7}8}
- ํ์ฌ ์ฐ๋ฆฌ์ ์ง์์ผ๋ก ์ด ์ฝ๋๊ฐ ์ ์๋ํ๋์ง ํ
์คํธํ๋ ค๋ฉด,
- ์ด ์ฝ๋๊ฐ ์คํ๋๋ Controller๋ฅผ ํตํด API ์ฝ์ ์ง์ ํ๋ ๋ฐฉ๋ฒ๋ฐ์ ์๋ค.
- ๋๋ฌด ์ค๋๊ฑธ๋ฆฌ๊ณ ์ผ๊ด์ฑ์ด ๋ถ์กฑํ๋ค.
- ์ด ์ฝ๋๋ฅผ ์์ฑํ์ง ์์ ์ฌ๋์ ์ด๋ป๊ฒ ํ ์คํธ ํด์ผํ ์ง ์ ์ ์๋ค. ์ด๋ค ๊ฐ๋ค๊น์ง ํ ์คํธ ํด์ผํ๋์ง ๋ฑ
- ์ ํํ ๋ก์ง์ ๋ฐ์ด๋๋ฆฌ๋ฅผ ์ ์ ์์ผ๋ ๋ค๋ฅธ ์ฌ๋์ด ์ด ์ฝ๋๋ฅผ ๋ณ๊ฒฝํ์๋,
- ๋ด๊ฐ ๋ง๊ฐ๋จ๋ฆฐ ๋ก์ง์ด ์๋์ง ์ฝ๊ฒ ํ์ธ์ด ๋ถ๊ฐ๋ฅํ๋ค.
- ๊ทธ๋์ ์ฐ๋ฆฌ๋ ์ฝ๋๋ฅผ ์ฝ๋๋ก ํ ์คํธํ๊ณ , ํ ์คํธ ์ฝ๋๋ผ๊ณ ๋ถ๋ฅธ๋ค.
1.2 Testing ์์
1import { Test, TestingModule } from '@nestjs/testing'2import { MathService } from './math.service'34describe('MathService', () => {5let service: MathService67beforeEach(async () => {8const module: TestingModule = await Test.createTestingModule({9providers: [MathService],10}).compile()1112service = module.get<MathService>(MathService)13})1415it('should be defined', () => {16expect(service).toBeDefined() // service๊ฐ ์ ์๋์ด ์๋์ง ํ์ธ17})1819it('should return correct sum of two numbers', () => {20const result = service.add(2, 3)21expect(result).toEqual(5) // ์ธํ์ ๋ํ ์์ํ์ ํ ์คํธ22})2324it('should handle negative numbers', () => {25const result = service.add(-1, 3)26expect(result).toEqual(-4) // ๋ค์ํ ์ผ์ด์ค๋ฅผ ํ ์คํธ27})28})
- ํ ์คํธ ์ฝ๋๋ ์ฝ๋์ ์๋ ๋ก์ง์ ๊ฒ์ฆํ๋ ์ฝ๋๋ค.
- ํํ ์ด๋ค ํน์ ๊ฐ์ด ์ ๋ ฅ๋์๋ ์ด๋ค ๊ฐ์ด ๋ฐํ๋๋์ง ํ์ธํ๋ค.
- ๋ค์ํ ๊ฐ๋ค์ ์ ๋ ฅ ํด๋ณผ ์ ์๋ค. ์์ ์์ ์ฒ๋ผ ์์ ๋ฟ ๋ง ์๋๋ผ ์์์ ๋ํ ๋ง์ ๋ ํ ์คํธ ํด๋ณผ ์ ์๋ค.
- ํน์ ๊ธฐ๋์น์ ๋ํ assert๊ฐ ๊ฐ๋ฅํ๋ค.
- expect()๋ฅผ ์ฌ์ฉํ assert๋ false๋ฅผ ๋ฐํํ ๋ ํ ์คํธ๊ฐ ์คํจํ๋ค.
- ์ต์ง๋ก ์ด์ํ ๊ฐ์ ๋ฃ์ด์ ๊ธฐ๋ํ๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋์ง ํ์ธ ๊ฐ๋ฅํ๋ค.
- ๊ฐ๋ฅํ๋ฉด ์๋๋ ๋ก์ง์ ๊ฒ์ฆ ํ ์ ์๋ค.
1.3 Testing ๊ฒฐ๊ณผ
1PASS src/math.service.spec.ts2MathService3โ should be defined (5 ms)4โ should return correct sum of two numbers (1 ms)5โ should handle negative numbers (1 ms)67Test Suites: 1 passed, 1 total8Tests: 3 passed, 3 total9Snapshots: 0 total10Time: 1.015 s
pnpm test์ปค๋งจ๋๋ฅผ ์ฌ์ฉํด์ ํ ์คํธ๋ฅผ ์คํํ๊ณ ๊ฒฐ๊ณผ ๊ฐ์ ๋ฐ์ ๋ณผ ์ ์๋ค.- ์ด๋ค ํ
์คํธ๊ฐ ์ฑ๊ณต/์คํจ ํ๋์ง์ ํจ๊ป ๋ช๊ฐ์ ํ
์คํธ๊ฐ ์ฑ๊ณต/์คํจ ํ๋์ง,
- ์คํํ๋๋ฐ ์ผ๋ง๋ ๊ฑธ๋ ธ๋์ง ๋ฑ์ ํต๊ณ ๋ฅผ ๋ฐ์ ์ ์๋ค
2. Matcher ํจ์
1test('two plus two is four', () => {2// expect() ํจ์์ ์๊ท๋จผํธ๊ฐ3// toBe() ํจ์์ ์๊ท๋จผํธ์ ๊ฐ์์ง ํ์ธํ๋ค4expect(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()
1const can1 = {2flavor: 'grapefruit',3ounces: 12,4}5const can2 = {6flavor: 'grapefruit',7ounces: 12,8}910describe('Test', () => {11test('have all the same properties', () => {12expect(can1).toEqual(can2)13})14test('are the exact same can', () => {15expect(can1).not.toBe(can2)16})17})
2.2 ์ซ์ Matcher
toBeGreaterThan(number): ๊ฐ์ด ๋ ํฐ์ง ํ์ธํ๋ค.toBeGreaterThanOrEqual(number): ๊ฐ์ด ๋ ํฌ๊ฑฐ๋ ๊ฐ์์ง ํ์ธํ๋ค.toBeLessThan(number): ๊ฐ์ด ๋ ์์์ง ํ์ธํ๋ค.toBeLessThanOrEqual(number): ๊ฐ์ด ๋ ์๊ฑฐ๋ ๊ฐ์์ง ํ์ธํ๋ค.toBeCloseTo(number, numDigits?): ํน์ ์์์ ๊น์ง ๊ฐ์ ๊ฐ์ธ์ง ํ์ธํ๋ค
2.2.1 toBeCloseTo()
1test('adding works sanely with decimals', () => {2// Fall! (0.30000000000000004 !== 0.3)3expect(0.2 + 0.1).toBe(0.3)4})
1test('adding works sanely with decimals', () => {2expect(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()
1function dringAll(callback, flavour) {2if (flavour !== 'octopus') {3callback(flavour)4}5}67describe('Test', () => {8test('drinks something lemon-flavoured', () => {9const drink = jest.fn()10dringAll(drink, 'lemon')11expect(drink).toHaveBeenCalled()12})1314test('does not drink something octopus-flavoured', () => {15const drink = jest.fn()16dringAll(drink, 'octopus')17expect(drink).not.toHaveBeenCalled()18})19})
2.3.2 toHaveNthReturnedWith()
1test('drink returns expected nth calls', () => {2const beverage1 = { name: 'Lemon' }3const beverage2 = { name: 'Orange' }4const drink = jest.fn(beverage => beverage.name)56drink(beverage1)7drink(beverage2)89expect(drink).toHaveNthReturnedWith(1, 'Lemon')10expect(drink).toHaveNthReturnedWith(2, 'Orange')11})
2.4 ๋ฐฐ์ด ๋ฐ ๊ฐ์ฒด Matcher
toContain(item): ๋ฐฐ์ด ๋๋ ๋ฌธ์์ด์ ํน์ ํญ๋ชฉ์ด ํฌํจ๋์ด ์๋์ง ํ์ธํ๋ค.toContainEqual(item): ๋ฐฐ์ด์ ๊ตฌ์กฐ์ ์ผ๋ก ๊ฐ์ ํญ๋ชฉ์ด ํฌํจ๋์ด ์๋์ง ํ์ธํ๋ค.toHaveLength(number): ๋ฐฐ์ด, ๋ฌธ์์ด ๋๋ ๊ฐ์ฒด์ ๊ธธ์ด/ํฌ๊ธฐ๊ฐ ํน์ ๊ฐ๊ณผ ์ผ์นํ๋์ง ํ์ธํ๋ค.toHaveProperty(keyPath, value?): ๊ฐ์ฒด์ ํน์ ๊ฒฝ๋ก์ ์์ฑ์ด ์กด์ฌํ๊ณ , ์ ํ์ ์ผ๋ก ํด๋น ์์ฑ์ ๊ฐ์ด ํน์ ๊ฐ๊ณผ ์ผ์นํ๋์ง ํ์ธํ๋ค.toMatchObject(object): ๊ฐ์ฒด๊ฐ ํน์ ๊ฐ์ฒด์ ๋ถ๋ถ์ ์ผ๋ก ์ผ์นํ๋์ง ํ์ธํ๋ค
2.4.1 toContainEqual()
1describe('my beverage', () => {2test('is delicious and not sour', () => {3const myBeverage = { delicious: true, sour: false }4expect([myBeverage, ...]).toContainEqual(myBeverage)5})6})
2.4.2 toHaveProperty()
1const houseForSale = {2bath: true,3bedrooms: 4,4kitchen: {5amenities: ['oven', 'stove', 'washer'],6area: 20,7wallColor: 'white',8'nice.oven': true,9},10liveingroomm: {11amenities: [12{13couch: [14['large', { dimensions: [20, 20] }],15['small', { dimensions: [10, 10] }],16],17},18],19},20'ceiling.height': 2,21}
1test('this house has my desired features', () => {2expect(houseForSale).toHaveProperty('bath')3expect(houseForSale).toHaveProperty('bedrooms', 4)45expect(houseForSale).not.toHaveProperty('pool')67// '.'๋ฅผ ์ฌ์ฉํด์ ๊น๊ฒ ๋ ํผ๋ฐ์ฑํ๊ธฐ8expect(houseForSale).toHaveProperty('kitchen.area', 20)9expect(houseForSale).toHaveProperty('kitchen.amenities', ['oven', 'stove', 'washer'])1011expect(houseForSale).not.toHaveProperty('kitchen.open')1213// Array๋ฅผ ์ฌ์ฉํด์ ๊น๊ฒ ๋ ํผ๋ฐ์ฑํ๊ธฐ14expect(houseForSale).toHaveProperty(['kitchen', 'area'], 20)15expect(houseForSale).toHaveProperty(16['kitchen', 'amenities'], //17['oven', 'stove', 'washer'],18)19expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], 'oven')20expect(houseForSale).toHaveProperty(21'liveingroomm.amenities[0].couch[0][1].dimensions[0]', //2220,23)24expect(houseForSale).toHaveProperty(['kitchen', 'nice.oven'])25expect(houseForSale).not.toHaveProperty(['kitchen', 'open'])2627// ํค๊ฐ ์์ฒด์ '.'์ด ์๋ ๊ฒฝ์ฐ Array ์ฌ์ฉ28expect(houseForSale).toHaveProperty(['ceiling.height'], 'tall')29})
2.5 ์๋ฌ Matcher
toThrow(error?): ํจ์๊ฐ ํธ์ถ๋ ๋ ํน์ ์ค๋ฅ๋ฅผ ๋์ง๋์ง ํ์ธํ๋ค
2.5.1 toThrow()
1function drinkFlavor(flavor) {2if (flavor === 'octopus') {3throw new DisgustingFlavorError('์ผ์ ๋ฌธ์ด ๋ ธ๋ง์ด์ผ')4}5}
1test('throws on octopus', () => {2function drinkOctopus() {3drinkFlavor('octopus')4}56// ์๋ฌ๋ฉ์์ง ์ด๋๊ฐ์ '๋ ธ๋ง'์ด๋ผ๊ณ ์จ์ ธ์๋์ง ํ์ธํ๋ค.7expect(drinkOctopus).toThrow(/๋ ธ๋ง/)8expect(drinkOctopus).toThrow('๋ ธ๋ง')910// ์ ํํ ๋ฌธ์ฅ์ ํ ์คํธํ๋ค.11expect(drinkOctopus).toThrow(/^์ผ์ ๋ฌธ์ด ๋ ธ๋ง์ด์ผ!$/)12expect(drinkOctopus).toThrow(new Error('์ผ์ ๋ฌธ์ด ๋ ธ๋ง์ด์ผ!'))1314// DisgustingFlavorError ํ์ ์ ์๋ฌ๊ฐ ๋์ ธ์ง๋๊ฑธ ํ์ธํ๋ค.15expect(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()
1describe('arrayContaining', () => {2const expected = ['Alice', 'Bob']34it('matches even if received contains additional elements', () => {5expect(['Alice', 'Bob', 'Eve']).toEqual(expect.arrayContaining(expected))6})7})
3. Modifiers
not: ๋ฐ๋ ํ ์คํธ๋ก ์ ํresolves: Promise ์ ์ ๋ฐํ์ผ๋ก ์ ํrejects: Promise ๋์ง๋ ์ํฉ์ผ๋ก ์ ํ
3.1 not
1test('flavor is not coconut', () => {2expect('apple').not.toBe('coconut')3})
3.2 resolve
1test('resolves to lemon', () => {2// make sure to add a return statement3return expect(Promise.resolve('lemon')).resolves.toBe('lemon')4})
3.3 reject
1test('rejects to octopus', () => {2return 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
1import { Test, TestingModule } from '@nestjs/testing'2import { UserService } from './user.service'3import { UserRepository } from './user.repository'45describe('UserService with Mock', () => {6let userService: UserService7let userRepositoryMock: { findById: jest.Mock }89beforeEach(async () => {10userRepositoryMock = { findById: jest.fn() } // Mock ์์ฑํ๊ธฐ1112const module: TestingModule = await Test.createTestingModule({13providers: [14UserService,15{16provide: UserRepository,17useValue: userRepositoryMock, // Mock ์ฃผ์ ํ๊ธฐ18},19],20}).compile()2122userService = module.get<UserService>(UserService)23})2425it('should call findById on UserRepository', () => {26const userId = '1'27userService.findUserById(userId)2829// ์คํ๋๊ฑธ ํ์ธ30expect(userRepositoryMock.findById).toHaveBeenCalledWith(userId)3132// ํ๋ฒ๋ง ๋ถ๋ฆฐ๊ฑธ ํ์ธ33expect(userRepositoryMock.findById).toHaveBeenCalledTimes(1)34})35})
4.2 Stub
1import { Test, TestingModule } from '@nestjs/testing'2import { UserService } from './user.service'3import { UserRepository } from './user.repository'45describe('UserService with Stub', () => {6let userService: UserService78beforeEach(async () => {9const userRepositoryStub = {10findById: (id: string) => ({ id, name: 'Stubbed User' }), // Stub ์์ฑํ๊ธฐ11}1213const module: TestingModule = await Test.createTestingModule({14providers: [15UserService,16{17provide: UserRepository,18useValue: userRepositoryStub, // Stub ์ฃผ์ ํ๊ธฐ19},20],21}).compile()2223userService = module.get<UserService>(UserService)24})2526it('should return the stubbed user', () => {27const userId = '1'28const result = userService.findUserById(userId)2930// ๋ณํ๊ฐ ๊ฒ์ฆ31expect(result).toEqual({ id: userId, name: 'Stubbed User' })32})33})
4.3 Fake
1import { Test, TestingModule } from '@nestjs/testing'2import { UserService } from './user.service'3import { UserRepository } from './user.repository'45// Faks ์์ฑ6class FakeUserRepository {7private user = [{ id: '1', name: 'Fake User' }]89findById(id: string) {10return this.users.find(user => user.id === id) || null11}12}1314describe('UserService with Fake', () => {15let userService: UserService1617beforeEach(async () => {18const module: TestingModule = await Test.createTestingModule({19providers: [20UserService,21{22provide: UserRepository,23useClass: FakeUserRepository, // Fake ์ฃผ์ ํ๊ธฐ24},25],26}).compile()2728userService = module.get<UserService>(UserService)29})3031it('should return the fake user', () => {32const userId = '1'33const result = userService.findUserById(userId)3435// ๊ฒฐ๊ณผ๊ฐ ๊ฒ์ฆ36expect(result).toEqual({ id: userId, name: 'Fake User' })37})3839it('should return null if user not found', () => {40const userId = '2'41const result = userService.findUserById(userId)4243// ๊ฒฐ๊ณผ๊ฐ ๊ฒ์ฆ44expect(result).toBeNull()45})46})
4.4 Mock vs Stub vs Fake
| ํญ๋ชฉ | Mock | Stub | Fake |
|---|---|---|---|
| ๋ชฉ์ | ๊ฐ์ฒด ๊ฐ์ ์ํธ์์ฉ๊ณผ ๋์์ ๊ฒ์ฆํ๋ค | ํน์ ๋ก์ง์ ํ
์คํธํ๊ธฐ์ํด ๋ฏธ๋ฆฌ ์ ์๋ ๊ฐ์ ๋ฐํํ๋ค | ์ต์ํ์ ๊ธฐ๋ฅ์ ๊ฐ์ ์ค์ ๊ฐ์ฒด๋ฅผ ์๋ฎฌ๋ ์ด์ ํ๋ค |
| ๋์ | ๋ฉ์๋ ํธ์ถ, ํ๋ผ๋ฏธํฐ ๋ฑ์ ํ์ธ | ์ ์ ์ด๊ณ ๋จ์ํ๊ณ ์ง์ ๋ ์์ํ์ ๋ฐํ | ์ค์ ๊ฐ์ฒด์ ๋์์ ๋ชจ๋ฐฉ |
| ๋ณต์ก๋ | ๋์ ์ด๋ฉฐ ๋ค์ํ ์ค์ ์ ํตํด ๋ณต์กํ๊ฒ ๋ง๋ค ์ ์๋ค | ๋์ฑ ๊ฐ๋จํ๋ฉฐ ์ ์ ์ธ ์๋ต์ ์ ๊ณตํ๋ค | ์คํ
๋ณด๋ค๋ ๋ณต์กํ์ง๋ง ์ค์ ๊ฐ์ฒด๋ณด ๋ค๋ ๋ ๋ณต์กํ๋ค |
| ์ฌ์ฉ ์์ | ๋ฉ์๋๊ฐ ํ
์คํธ์์ ์ฌ๋ฐ๋ฅด๊ฒ ํธ์ถ๋๋์ง ํ์ธํ๋ค | ํน์ ๋ฐํ ๊ฐ์ ์ ๊ณตํ์ฌ ๋จ์ ํ ์คํธ๋ฅผ ๊ฒฉ๋ฆฌํ๋ค. | ํ
์คํธ๋ฅผ ์ํด ์ค์ ์ข
์์ฑ์์ด ์ต์ํ์ ๊ตฌํ์ ์ ๊ณตํ๋ค |
4.5 Mock Function
1import { Test, TestingModule } from '@nestjs/testing'2import { UserService } from './user.service'3import { UserRepository } from './user.repository'45describe('UserService with Mock', () => {6let userService: UserService7let userRepositoryMock: { findById: jest.Mock }89beforeEach(async () => {10userRepositoryMock = { findById: jest.fn() } // Mock ์์ฑํ๊ธฐ1112const module: TestingModule = await Test.createTestingModule({13providers: [14UserService,15{16provide: UserRepository,17useValue: userRepositoryMock, // Mock ์ฃผ์ ํ๊ธฐ18},19],20}).compile()2122userService = module.get<UserService>(UserService)23})2425it('should call findById on UserRepository', () => {26const userId = '1'27userService.findUserById(userId)2829// ์คํ๋๊ฑธ ํ์ธ30expect(userRepositoryMock.findById).toHaveBeenCalledWith(userId)3132// ํ๋ฒ๋ง ๋ถ๋ฆฐ๊ฑธ ํ์ธ33expect(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
1const mockFn = jest.fn()23const a = new mockFn()4const b = new mockFn()56mockFn.mock.instances[0] === a // true7mockFn.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()
1const mockFn = jest.fn(scalar => 42 + scalar)23mockFn(0) // 424mockFn(1) // 4356mockFn.mockImplementation(scalar => 36 + scalar)78mockFn(2) // 389mockFn(3) // 39
4.10 mockReturnThis()
1jest.fn(function () {2return this3})
4.11 mockReturnValue()
1jest.fn().mockImplementation(() => value)
4.12 mockResolvedValue()
1jest.fn().mockImplementation(() => Promise.resolve(value))
4.13 mockRejectedValue()
1jest.fn().mockImplementation(() => Promise.reject(value))
4.14 Mock Function ๊ตฌํ
mockFn.mockClear(): mock function์ ํธ์ถ ๊ธฐ๋ก๊ณผ ๋ฐํ ๊ฐ๋ค์ ์ง์ด๋ค (์ํ ์ด๊ธฐํ).mockFn.mockReset(): mockClear()์ ๊ธฐ๋ฅ์ ๋ชจ๋ ์คํํ๊ณ mock ํจ์๋ฅผ ๋น ํจ์๋ก ๋์ฒดํ๋ค.mockFn.mockRestore(): mockReset()์ ์์ ์ ๋ชจ๋ ์งํํ๊ณ mock ํจ์๋ฅผ ์๋ ๊ตฌํ์ฒด๋ก ๋ณต์ํ๋ค
5. ํ ์คํ ์ ์ข ๋ฅ

Unit Testing: ํจ์๋ ํด๋์ค์ฒ๋ผ ๊ฐ์ฅ ์์ ๋จ์์ ๋ก์ง์ โ๋ ๋ฆฝ์ ์ผ๋กโ ํ ์คํธํ๋คIntegration Testing: ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฑ ๋ค์ํ ์๋น์ค์ ์์๋ค์ ํจ๊ป ์คํ ํ์๋ ๋ฌธ์ ๊ฐ ์๋์ง ํ์ธํ๋คEnd to End Testing: ์ฌ์ฉ์์ ๊ด์ ์์ ์๋น์ค๋ฅผ ์ฌ์ฉ ํ์๋ ํ๋ก๊ทธ๋จ์ด ์ ์์ ์ผ๋ก ์๋ํ๋์ง ํ์ธํ๋ค
5.1 NestJS Testing ์์
1import { CatsController } from './cats.controller'2import { CatsService } from './cats.service'34describe('CatsController', () => {5let catsController: CatsController6let catsService: CatsService78beforeEach(() => {9catsService = new CatsService()10catsController = new CatsController(catsService)11})1213describe('findAll', () => {14it('should return an array of cats', async () => {15const result = ['test']16jest.spyOn(catsService, 'findAll').mockImplementation(() => result)1718expect(await catsController.findAll()).toBe(result)19})20})21})
1import { Test } from '@nestjs/testing'2import { CatsController } from './cats.controller'3import { CatsService } from './cats.service'45describe('CatsController', () => {6let catsController: CatsController7let catsService: CatsService89beforeEach(async () => {10const module = await Test.createTestingModule({11controllers: [CatsController],12providers: [CatsService],13}).compile()1415catsService = modulRef.get<CatsService>(CatsService)16catsController = moduleRef.get<CatsController>(CatsController)17})1819describe('findAll', () => {20it('should return an array of cats', async () => {21const result = ['test']22jest.spyOn(catsService, 'findAll').mockImplementation(() => result)2324expect(await catsController.findAll()).toBe(result)25})26})27})
5.2 Unit Testing ์์
1@Controller('uesrs')2export class UsersController {3constructor(private readonly usersService: UsersService) {}45@Get(':id')6async getUserById(@Param('id', ParseIntPipe) id: number): Promise<User> {7return this.usersService.findUserById(id)8}9}
1@Injectable()2export class UsersService {3constructor(4@InjectRepository(User)5private readonly userRepository: Repository<User>,6) {}78async findUserById(id: number): Promise<User> {9if (id === '2') return null1011return await this.userRepository.findOne({12where: { id },13})14}15}
1describe('UserController', () => {2let usersController: UsersController3let usersService: UsersService45beforeEach(async () => {6const module: TestingModule = await Test.createTestingModule({7controllers: [UsersController],8providers: [9{10provide: UsersService,11useValue: {12findUserById: jest.fn(),13},14},15],16}).compile()1718usersService = module.get<UsersService>(UsersService)19usersController = module.get<UsersController>(UsersController)20})2122it('should call UserService.findUserById with the correct parameter', async () => {23const id = '1'24const userServiceSpy = jest25.spyOn(usersService, 'findUserById') //26.mockResolvedValueOnce({ id, name: 'John Doe' })27const result = userController.getUserById(id)2829expect(userServiceSpy).toHaveBeenCalledWith(id)30expect(result).toEqual({ id, name: 'John Doe' })31})32})
5.3 Integration Testing ์์
1@Controller('users')2export class UsersController {3constructor(private readonly usersService: UsersService) {}45@Get(':id')6async getUserById(@Param('id', ParseIntPipe) id: number): Promise<User> {7return this.usersService.findUserById(id)8}9}
1@Injectable()2export class UsersService {3constructor(4@InjectRepository(User)5private readonly userRepository: Repository<User>,6) {}78async findUserById(id: number): Promise<User> {9if (id === '2') return null1011return await this.userRepository.findOne({12where: { id },13})14}15}
1describe('UserController (Integration)', () => {2let userController: UsersController3let usersService: UsersService45beforeEach(async () => {6const module: TestingModule = await Test.createTestingModule({7controllers: [UsersController],8providers: [UsersService],9}).compile()1011userController = module.get<UsersController>(UsersController)12usersService = module.get<UsersService>(UsersService)13})1415it('should return user data for a valid ID', () => {16const id = '1'17const result = userController.getUserById(id)18expect(result).toEqual({ id: '1', name: 'John Doe' })19})2021it('should return null for an invalid ID', () => {22const id = '2'23const result = userController.getUserById(id)24expect(result).toBeNull()25})26})
5.4 End to End Testing ์์
1describe('UserController (E2E)', () => {2let app: INestApplication34beforeAll(async () => {5const moduleFixture: TestingModule = await Test.createTestingModule({6imports: [AppModule],7}).compile()89app = moduleFixture.createNestApplication()10await app.init()11})1213it('/users/:id (GET) - should return user data for a valid ID', async () => {14const id = '1'15const response = await request(app.getHttpServer()).get(`/users/${id}`).expect(200)1617expect(response.body).toEqual({ id: '1', name: 'John Doe' })18})1920it('/users/:id (GET) - should return 404 for an invalid ID', async () => {21const id = '2'22await request(app.getHttpServer()).get(`/users/${id}`).expect(404)23})2425afterAll(async () => {26await 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// Statement2const sum = (a, b) => a + b3// Statement4console.log(sum(1, 2))
6.2 Coverage (Branch)
1if (x > 10) {2// ํ๋์ ๋ถ๊ธฐ3console.log('x๋ 10๋ณด๋ค ํฝ๋๋ค')4} else {5// ํ๋์ ๋ถ๊ธฐ6console.log('x๋ 10 ์ดํ์ ๋๋ค')7}
6.3 Coverage (Function)
1function add(a, b) {2// ํจ์์ ๋๋ค.3return a + b4}56function subtract(a, b) {7// ํจ์์ ๋๋ค.8return a - b9}
6.4 Coverage (Line)
1const multiply = (a, b) => {2// ๋ผ์ธ์ ๋๋ค.3return a * b4}56// ๋ผ์ธ7console.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 ๋ฆฌ์คํธ์ ๋ฃ์ด๋ฒ๋ฆฌ์