반응형

NestJS + Jest 테스트에서 describe, beforeEach, spyOn을 활용하여 의존성을 주입하고 Mock 데이터를 설정하는 방법

🔍 개요

NestJS에서 Jest를 사용해 테스트 코드를 작성할 때,

  • describe()를 사용해 테스트를 계층적으로 나누고,
  • beforeEach()에서 공통 의존성을 주입하며,
  • jest.spyOn()을 활용해 비즈니스 로직 내부의 특정 메서드를 Mocking하여 원하는 데이터를 설정할 수 있습니다.

이 글에서는 NestJS 서비스의 비즈니스 로직을 단위 테스트하는 과정에서 의존성을 주입하고, Mock 데이터를 설정하는 올바른 방법을 설명합니다.


1️⃣ Jest에서 의존성 주입과 Mock 데이터 설정이 필요한 이유

NestJS에서는 의존성 주입(Dependency Injection, DI)을 사용하여 서비스 간의 의존성을 관리합니다.
하지만 Jest에서 단위 테스트를 작성할 때, 실제 서비스 대신 Mock 데이터를 주입해서 테스트합니다.

이유는 다음과 같습니다:

  1. 독립적인 테스트를 수행하기 위해
    • 실제 데이터베이스나 API 호출 없이 테스트를 실행해야 합니다.
  2. 테스트 실행 속도를 높이기 위해
    • 실제 API나 DB를 호출하면 테스트가 느려지고, 불필요한 비용이 발생합니다.
  3. 예측 가능한 테스트를 위해
    • jest.spyOn()을 사용하여 특정 메소드의 반환값을 제어할 수 있습니다.

2️⃣ NestJS에서 Jest 테스트 환경 설정

📌 1. 기본적인 테스트 코드 구조

다음은 NestJS 서비스에서 Jest를 사용한 기본적인 테스트 코드 구조입니다.
샘플 코드) Android 인앱결제 신호를 받는 비지니스 로직 테스트 코드

import { Test, TestingModule } from '@nestjs/testing';
import { AndroidRtdnCommandHandler } from './android-rtdn-command.handler';
import { IAPValidatorProvider } from '../providers/iap-validator.provider';
import { SubscriptionRepository } from '../subscription.repository';
import { AppConfigService } from 'src/config';

describe('AndroidRtdnCommandHandler', () => {
  let handler: AndroidRtdnCommandHandler;
  let iapValidatorProvider: jest.Mocked<IAPValidatorProvider>;
  let subscriptionRepository: jest.Mocked<SubscriptionRepository>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AndroidRtdnCommandHandler,
        {
          provide: IAPValidatorProvider,
          useValue: { validateGooglePurchase: jest.fn() }, // Mock 설정
        },
        {
          provide: SubscriptionRepository,
          useValue: { findByLatestOrderId: jest.fn() },
        },
      ],
    }).compile();

    handler = module.get<AndroidRtdnCommandHandler>(AndroidRtdnCommandHandler);
    iapValidatorProvider = module.get<IAPValidatorProvider>(IAPValidatorProvider);
    subscriptionRepository = module.get<SubscriptionRepository>(SubscriptionRepository);
  });

  describe('processGooglePlayMessage', () => {
    let validateGooglePurchaseSpy: jest.SpyInstance;
    let findByLatestOrderIdSpy: jest.SpyInstance;

    beforeEach(() => {
      validateGooglePurchaseSpy = jest
        .spyOn(iapValidatorProvider, 'validateGooglePurchase')
        .mockResolvedValue({
          latestOrderId: "test-order-id",
          subscriptionState: 2,
          testPurchase: false,
        });

      findByLatestOrderIdSpy = jest
        .spyOn(subscriptionRepository, 'findByLatestOrderId')
        .mockResolvedValue({
          id: "test-id",
          orderId: "test-order-id",
          state: "active",
        } as unknown as Subscription);
    });

    it('should call handleSubscriptionValidate for subscriptionNotification', async () => {
      const message = {
        subscriptionNotification: {
          notificationType: 1,
          purchaseToken: "test-token",
          subscriptionId: "1month_subscription",
        }
      };

      await handler.processGooglePlayMessage(message);

      expect(validateGooglePurchaseSpy).toHaveBeenCalled();
      expect(findByLatestOrderIdSpy).toHaveBeenCalled();
    });
  });
});

3️⃣ Jest에서 의존성을 주입하는 방법

📌 Test.createTestingModule을 사용해 의존성 주입

Test.createTestingModule()을 사용하면 NestJS의 providers를 Mock 객체로 대체하여 테스트 환경을 구성할 수 있습니다.

예제: beforeEach에서 의존성 주입

beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    providers: [
      AndroidRtdnCommandHandler,
      {
        provide: IAPValidatorProvider,
        useValue: { validateGooglePurchase: jest.fn() }, // Mock 설정
      },
      {
        provide: SubscriptionRepository,
        useValue: { findByLatestOrderId: jest.fn() },
      },
    ],
  }).compile();

  handler = module.get<AndroidRtdnCommandHandler>(AndroidRtdnCommandHandler);
  iapValidatorProvider = module.get<IAPValidatorProvider>(IAPValidatorProvider);
  subscriptionRepository = module.get<SubscriptionRepository>(SubscriptionRepository);
});

이렇게 설정하면 실제 서비스 대신 Mock 객체가 주입되므로, 테스트가 독립적으로 실행될 수 있습니다.


4️⃣ jest.spyOn()을 사용하여 특정 메소드 Mocking

📌 jest.spyOn()을 사용해 특정 함수의 반환값을 변경

단위 테스트에서는 비즈니스 로직이 내부에서 실행하는 특정 메소드를 Mocking할 필요가 있습니다.

예제: 특정 함수의 반환값을 변경

const validateGooglePurchaseSpy = jest
  .spyOn(iapValidatorProvider, 'validateGooglePurchase')
  .mockResolvedValue({
    latestOrderId: "test-order-id",
    subscriptionState: 2,
    testPurchase: false,
  });

const findByLatestOrderIdSpy = jest
  .spyOn(subscriptionRepository, 'findByLatestOrderId')
  .mockResolvedValue({
    id: "test-id",
    orderId: "test-order-id",
    state: "active",
  } as unknown as Subscription);

이렇게 하면 테스트에서 특정 함수의 반환값을 원하는 값으로 설정할 수 있습니다.


5️⃣ jest.spyOn()을 사용해 함수 호출 여부 검증

테스트 실행 후 특정 함수가 정상적으로 실행되었는지 확인해야 합니다.

📌 expect(...).toHaveBeenCalled()을 사용

it('should call handleSubscriptionValidate for subscriptionNotification', async () => {
  const message = {
    subscriptionNotification: {
      notificationType: 1,
      purchaseToken: "test-token",
      subscriptionId: "1month_subscription",
    }
  };

  await handler.processGooglePlayMessage(message);

  expect(validateGooglePurchaseSpy).toHaveBeenCalled(); // ✅ 함수가 호출되었는지 확인
  expect(findByLatestOrderIdSpy).toHaveBeenCalled();
});

이제 이 테스트는 특정 메소드가 호출되었는지 검증할 수 있습니다.


🚀 결론

✔ Jest에서 의존성을 주입하고 Mock 데이터를 설정하는 Best Practice

  1. Test.createTestingModule()을 사용해 Mock 서비스를 주입
  2. beforeEach()에서 공통적으로 필요한 Mock 설정을 적용
  3. jest.spyOn()을 사용해 내부 메소드의 반환값을 설정
  4. expect(...).toHaveBeenCalled()을 사용해 함수 호출 여부를 검증
  5. 중복된 Mock 설정을 beforeEach()에서 한 번만 설정하여 유지보수를 쉽게 만듦

💡 이렇게 하면?

  • 독립적인 단위 테스트 가능
  • 외부 API나 DB 호출 없이 빠른 테스트 실행
  • 비즈니스 로직이 의도한 대로 동작하는지 검증 가능
728x90
반응형

'NestJS' 카테고리의 다른 글

[Nest.js] PIPE  (0) 2023.08.27
[Nest.js] TypeORM 이용한 CRUD  (0) 2023.08.22
[Nest.js] 기본 파일 구조 및 요청 흐름  (2) 2023.08.20
[Nest.js] Quick Start!  (0) 2023.08.15
반응형

깨끗한 테스트 코드

 

목차

1. 테스트 코드의 중요성

2. 테스트의 종류

3. Unit Test 작성

4. FIRST 원칙

5. 오픈소스 속 Unit Test


1. 테스트 코드의 중요성

  - 테스트 코드는 실수를 바로잡아준다.

  - 테스트 코드는 반드시 존재해야하며, 실제 코드 못지 않게 중요하다.

  - 테스트 케이스는 변경이 쉽도록 한다. 코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위테스트다.

  - 테스트 케이스가 있으면 변경이 두렵지 않다. 테스트 케이스가 없다면 모든 변경이 잠정적인 버그다. 테스트 커버리지가 높을수록 버그        에 대한 공포가 줄어든다.

  - 지저분한 테스트 코드는 테스트를 안하니만 못하다.

 

<Effective Unit Test 책에서>

'테스트는 실사용에 적합한 설계를 끌어내준다.'

'테스트를 작성해서 얻게 되는 가장 큰 수확은 테스트 자체가 아니다. 작성 과정에서 얻는 깨달음이다.'

 

'테스트트 자동화되어야 한다.' - 매번 배포할 때마다 실행되어야 한다.

 

2. 테스트의 종류

Unit Test : 프로그램 내부의 개발 컴포넌트의 동작을 테스트한다. 배포하기 전에 자동으로 실행되도록 많이 사용한다.

Integration Test : 프로그램 내부의 개별 컴포넌트들을 합쳐서 동작을 테스트한다. Unit Test는 각 컴포넌트를 고립시켜 테스트하기 때문에 컴포넌트의 interaction을 확인하는 Integration Test가 필요하다.

E2E Test : End to End Test. 실제 유저의 시나이로대로 네트워크를 통해 서버의 Endpoint를 호출해 테스트한다.

 

3. Unit Test 작성

'테스트 라이브러리를 사용하자' (실무에서 JUnit5 + mockito를 많이 사용한다.)

 

Test Double

  * 테스트에서 원본 객체를 대신하는 객체

 

Stub :   - 원래의 구현을 최대한 단순한 것으로 대체한다.  - 테스트를 위해 프로그래밍된 항목에만 응답한다.

 

Spy :

  - Stub의 역할을 하면서 호출에 대한 정보를 기록한다.

  - 이메일 서비스에서 메시지가 몇 번 정송되는지 확인할 때

 

Mock :

  - 행위를 검증하기 위해 가짜 객체를 만들어 테스트하는 방법

  - 호출에 대한 동작을 프로그래밍할 수 있다.

  - Stub은 상태를 검증하고 Mock은 행위를 검증한다.

 

'given, when, then 패턴을 사용하자'

  - given : 테스트를 위한 pre-condition

  - when : 테스트하고 싶은 동작 호출

  - then : 테스트 결과 확인

4. FIRST 원칙

Fast : 빠르게

  - 테스트는 빨리 돌아야 한다. 자주 돌려야 하기 때문이다.

Independent : 독립적으로

  - 각 테스트를 독립적으로 작성한다. 서로에게 의존하면 실패한 원인을 찾기 어려워진다.(다른 테스트의 샐패로 인한건지, 코드 오류인지)

Repeatable : 반복가능하게

  - 테스트는 어떤 환경에서도 반복 가능해야 한다. 실제 환경, QA 환경, 모든 환경에서 돌아가야 한다.

Self-Validating : 자가검증하는

  - 테스트는 bool 값으로 결과를 내야 한다.

Timely : 적시에

  - 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

 

4. 오픈소스 속 Unit Test

Trino(PrestoSQL) 프로젝트 코드

  - 책에서는 하나의 테스트에 하나의 assert를 사용하라고 했다.

  - 테스트하려는 인자가 많은 경우는 하나에 몰아서 하는 경우도 많다.

 

public class TestTypeCalculation
{
    @Test
    public void testBasicUsage()
    {
        assertEquals(Long.valueOf(42), calculateLiteralValue("42", ImmutableMap.of()));
        assertEquals(Long.valueOf(0), calculateLiteralValue("NULL", ImmutableMap.of()));
        assertEquals(Long.valueOf(0), calculateLiteralValue("null", ImmutableMap.of()));
        assertEquals(Long.valueOf(42), calculateLiteralValue("x", ImmutableMap.of("x", 42L)));
        assertEquals(Long.valueOf(42), calculateLiteralValue("(42)", ImmutableMap.of()));
        assertEquals(Long.valueOf(0), calculateLiteralValue("(NULL)", ImmutableMap.of()));
        assertEquals(Long.valueOf(42), calculateLiteralValue("(x)", ImmutableMap.of("x", 42L)));

        assertEquals(Long.valueOf(42 + 55), calculateLiteralValue("42 + 55", ImmutableMap.of()));
        assertEquals(Long.valueOf(42 - 55), calculateLiteralValue("42 - 55", ImmutableMap.of()));
        assertEquals(Long.valueOf(42 * 55), calculateLiteralValue("42 * 55", ImmutableMap.of()));
        assertEquals(Long.valueOf(42 / 6), calculateLiteralValue("42 / 6", ImmutableMap.of()));

        assertEquals(Long.valueOf(42 + 55 * 6), calculateLiteralValue("42 + 55 * 6", ImmutableMap.of()));
        assertEquals(Long.valueOf((42 + 55) * 6), calculateLiteralValue("(42 + 55) * 6", ImmutableMap.of()));

        assertEquals(Long.valueOf(2), calculateLiteralValue("min(10,2)", ImmutableMap.of()));
        assertEquals(Long.valueOf(10), calculateLiteralValue("min(10,2*10)", ImmutableMap.of()));
        assertEquals(Long.valueOf(20), calculateLiteralValue("max(10,2*10)", ImmutableMap.of()));
        assertEquals(Long.valueOf(10), calculateLiteralValue("max(10,2)", ImmutableMap.of()));

        assertEquals(Long.valueOf(42 + 55), calculateLiteralValue("x + y", ImmutableMap.of("x", 42L, "y", 55L)));
    }
}

 

code from - https://github.com/trinodb/trino/blob/master/core/trino-parser/src/test/java/io/trino/type/TestTypeCalculation.java

 

Junit5 Samples

  - @DisplayName은 테스트 클래스나 메서드에 보여질 이름을 입력하는 것이다. 테스트의 목적을 명확하게 작정할수 있다.

  - @ParameterizedTest는 하나의 테스트 메서드로 여러 가지 paramter를 테스트할 수 있다. @CsvSource의 값을 parameter로 넘       긴다.

  - JUnit5가 테스트에 관한 유용한 기능을 많이 가지고 있기 때문에 실무에서 많이 사용한다.

 

class CalculatorTests {

	@Test
	@DisplayName("1 + 1 = 2")
	void addsTwoNumbers() {
		Calculator calculator = new Calculator();
		assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2");
	}

	@ParameterizedTest(name = "{0} + {1} = {2}")
	@CsvSource({
			"0,    1,   1",
			"1,    2,   3",
			"49,  51, 100",
			"1,  100, 101"
	})
	void add(int first, int second, int expectedResult) {
		Calculator calculator = new Calculator();
		assertEquals(expectedResult, calculator.add(first, second),
				() -> first + " + " + second + " should equal " + expectedResult);
	}
}

 

code from - https://github.com/junit-team/junit5-samples/blob/main/junit5-jupiter-starter-gradle/src/test/java/com/example/project/CalculatorTests.java

 

728x90
반응형

'Book > Clean Code' 카테고리의 다른 글

[Clean Code] Chapter 11  (0) 2022.03.27
[Clean Code] Chapter 10  (0) 2022.03.26
[Clean Code] Chapter 08  (0) 2022.03.20
[Clean Code] Chapter 07  (0) 2022.03.17
[Clean Code] Chapter 06  (0) 2022.03.13

+ Recent posts