20. 테스트

미션 크리티컬한 소프트웨어 개발에서 자동화된 테스트는 필수적이다. 테스트를 자동화하면 개별테스트 및 테스트 모음을 빠르고 쉽게 반복할 수 있다. 이는 릴리스 품질 및 성능 목표를 충족하는지 확인 가능하게 한다. 자동화 테스트는 적용 범위를 늘리고 개발자에게 더 빠른 피드백 루프르 제공한다. 자동화 테스트는 개별 개발자의 생산성을 높이고 소스코드 제어 체크인, 기능 통합 및 버전 릴리즈와 같은 중요한 소프트웨어 개발 수명 주기 시점에 꼭 필요한 단계이다.

 

이러한 테스트는 단위 테스트, 엔드투엔드(e2e) 테스트, 통합 테스트 등 다양한 유형에 걸쳐있다, Nest는 효과적인 테스트를 포함한 개발 모범 사례를 제공하기 위해 다음과 같은 기능이 포함되어 있다.

 

  • 구성요소에 대한 기본 단위 테스트와 애플리케이션에 대한 e2e 테스트를 자동으로 스캐폴드.
  • 기본 도구 제공(예: 격리된 모듈/애플리케이션 로더를 구축하는 테스트 실행기)
  • 테스트 도구에 구애받지 않으면서도 즉시 사용 가능한 Jest  Supertest 와의 통합을 제공.
  • 쉽게 구성 요소를 모의할 수 있도록 테스트 환경에서 Nest 종속성 주입 시스템을 사용.

설치

npm i --save-dev @nestjs/testing
  •  

단위테스트

다음 예에서는 CatsController및 CatsService 2개의 클래스를 테스트한다. 앞서 언급했듯이 Jest는 기본 테스트 프레임워크로 제공된다. 테스트 실행기 역할을 하며 재연, 감시 등에 도움이 되는 어설션 기능과 테스트 이중 유틸리티도 제공한다.

// cats.controller.spec.ts

import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

describe('CatsController', () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(() => {
    catsService = new CatsService();
    catsController = new CatsController(catsService);
  });

  describe('findAll', () => {
    it('should return an array of cats', async () => {
      const result = ['test'];
      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

힌트) 테스트 파일을 테스트하는 클래스 근처에 두는게 좋다. 또한 테스트 파일에는 .spec 또는 .test 접미사가 있어야 한다.

 

위 샘플은 실제로 Nest와 관련된 어떤 것도 테스트하지 않는다. 실제로 우리는 종속성 주입도 사용하지 않고있다( CatsService 인스턴스를 catsController 에 전달한다는 점에 유의하자 ). 테스트 중인 클래스를 수동으로 인스턴스화하는 이러한 형태의 테스트는 프레임워크와 독립적이므로 격리 된 테스트 라고도 합니다. Nest 기능을 보다 광범위하게 사용하는 애플리케이션을 테스트하는 데 도움이 되는 몇 가지 고급 기능을 소개한다.

 

테스트 유틸리티

@nestjs/testing패키지는 보다 강력한 테스트 프로세스를 가능하게 하는 유틸리티 세트를 제공한다. 내장 Test클래스를 사용하여 이전 예제를 다시 작성하면 이렇다.

// cats.controller.spec.ts

import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

describe('CatsController', () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
        controllers: [CatsController],
        providers: [CatsService],
      }).compile();

    catsService = moduleRef.get<CatsService>(CatsService);
    catsController = moduleRef.get<CatsController>(CatsController);
  });

  describe('findAll', () => {
    it('should return an array of cats', async () => {
      const result = ['test'];
      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

 Test클래스는 기본적으로 전체 Nest 런타임을 모의하는 애플리케이션 실행 컨텍스트를 제공하는 데 유용할 뿐만 아니라, 모의 및 재정의를 포함하여 클래스 인스턴스를 쉽게 관리할 수 있는 훅을 제공한다. Test 클래스에는 모듈 메타데이터 개체를 인수( @Module() 데코레이터에 전달하는 것과 동일한 개체)로 사용하는 createTestingModule() 메서드가 있다 . 이 메서드는 몇 가지 메서드를 제공하는 TestingModule 인스턴스를 반환한다. 단위 테스트에서 중요한 것은 compile() 메소드이다 . 이 메서드는 종속성을 사용하여 모듈을 부트스트랩하고( NestFactory.create() 를 사용하여 기존 파일 main.ts 에서 애플리케이션을 부트스트랩하는 방식과 유사 ) 테스트할 준비가 된 모듈을 반환한다.

 

힌트) 이 compile()메서드는 비동기식 이므로 awaited 된다. 모듈이 컴파일되면 get() 메서드를 사용하여 선언된 정적 인스턴스(컨트롤러 및 공급자)를 검색할 수 있다 .

 

TestingModule 모듈 참조 클래스 에서 상속되므로 범위가 지정된 공급자(일시적 또는 요청 범위)를 동적으로 확인하는 기능이 있다. resolve() 메소드를 사용하여 이를 수행한다. ( get()메소드는 정적 인스턴스만 검색할 수 있다).

const moduleRef = await Test.createTestingModule({
  controllers: [CatsController],
  providers: [CatsService],
}).compile();

catsService = await moduleRef.resolve(CatsService);

주의) resolve() 메서드는 자체DI 컨테이너 하위 트리에서 공급자의 고유 인스턴스를 반환한다 . 각 하위 트리에는 고유한 컨텍스트 식별자가 있기때문에 이 메서드를 두 번 이상 호출하고 인스턴스 참조를 비교하면 동일하지 않음을 알 수 있다.

 

공급자의 프로덕션 버전을 사용하는 대신 테스트 목적으로 사용자 지정 공급자 로 재정의할 수 있다. 예를 들어, 라이브 데이터베이스에 연결하는 대신 데이터베이스 서비스를 모의할 수 있다. 다음 섹션에서 재정의를 다루겠지만 단위 테스트에도 사용할 수 있다.

 

자동 모의(Mocking)

Nest를 사용하면 누락된 모든 종속 항목에 적용할 모의 팩토리를 정의할 수도 있다. 이는 클래스에 많은 수의 종속성이 있고 이를 모두 모듸하는데 시간이 오래 걸리고 많은 설정이 필요한 경우에 유용하다. 이 기능을 사용하려면 종속성 모의에 대한 팩토리를 전달하는 createTestingModule()는  useMocker() 메서드는 연결되어야 한다 . 이 팩토리는 인스턴스 토큰인 선택적 토큰, Nest 공급자에게 유효한 모든 토큰을 가져와 모의 구현을 반환할 수 있다. 아래는 jest-mock 을 사용하여 일반 모의 객체를 생성하는 예와 jest.fn()  을 사용하여 CatsService 를 위한 특정 모의 객체를 생성하는 예이다.

// ...
import { ModuleMocker, MockFunctionMetadata } from 'jest-mock';

const moduleMocker = new ModuleMocker(global);

describe('CatsController', () => {
  let controller: CatsController;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      controllers: [CatsController],
    })
      .useMocker((token) => {
        const results = ['test1', 'test2'];
        if (token === CatsService) {
          return { findAll: jest.fn().mockResolvedValue(results) };
        }
        if (typeof token === 'function') {
          const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
          const Mock = moduleMocker.generateFromMetadata(mockMetadata);
          return new Mock();
        }
      })
      .compile();

    controller = moduleRef.get(CatsController);
  });
});

또한 일반적인 사용자 공급자 moduleRef.get(CatsService) 처럼 테스트 컨테이너에서 이러한 모의객체를 검색할 수 있다.

 

힌트) @golevelup/ts-jest 로 부터 createMock 같은 일반 모의 팩토리도 직접 전달할 수 있다.

 

힌트) REQUEST, INQUIRER 공급자는 컨텍스트에 이미 사전 정의되어 있으므로 자동으로 모의할 수 없다. 그러나 사용자 지정 공급자 구문을 사용하거나 .overrideProvider 메서드를 활용하여 오버라이드 할 수 있다.

 

엔드투엔드 테스트

개별 모듈 및 클래스에 초점을 맞추는 단위 테스트와 달리, e2e(엔드 투 엔드) 테스트는 보다 종합적인 수준에서 클래스와 모듈의 상호 작용을 다룬다. 이는 최종 사용자가 프로덕션 환경에서 갖게 되는 상호 작용 종류에 더 가깝다. 애플리케이션이 성장함에 따라 각 API 엔드포인트의 엔드투엔드 동작을 수동으로 테스트하기가 어려워진다. 자동화된 엔드 투 엔드 테스트는 시스템의 전반적인 동작이 올바르고 프로젝트 요구 사항을 충족하는지 확인하는 데 도움이 된다. e2e 테스트를 수행하기 위해 단위 테스트 에서 다룬 것과 유사한 구성을 사용한다 . 또한 Nest를 사용하면 Supertest 라이브러리를 사용하여 HTTP 요청을 쉽게 시뮬레이션할 수 있다.

import * as request from 'supertest';
// cats.e2e-spec.ts

import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';

describe('Cats', () => {
  let app: INestApplication;
  let catsService = { findAll: () => ['test'] };

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [CatsModule],
    })
      .overrideProvider(CatsService)
      .useValue(catsService)
      .compile();

    app = moduleRef.createNestApplication();
    await app.init();
  });

  it(`/GET cats`, () => {
    return request(app.getHttpServer())
      .get('/cats')
      .expect(200)
      .expect({
        data: catsService.findAll(),
      });
  });

  afterAll(async () => {
    await app.close();
  });
});

힌트) Fastify를 HTTP 어댑터로 사용하는 경우 약간 다른 구성이 필요하며 내장된 테스트 기능이 있다.

let app: NestFastifyApplication;

beforeAll(async () => {
  app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());

  await app.init();
  await app.getHttpAdapter().getInstance().ready();
});

it(`/GET cats`, () => {
  return app
    .inject({
      method: 'GET',
      url: '/cats',
    })
    .then((result) => {
      expect(result.statusCode).toEqual(200);
      expect(result.payload).toEqual(/* expectedPayload */);
    });
});

afterAll(async () => {
  await app.close();
});

이 예에서는 앞서 설명한 몇 가지 개념을 기반으로 gks다. 이전에 사용한 방법 compile() 외에도 이제 이 createNestApplication() 메서드를 사용하여 전체 Nest 런타임 환경을 인스턴스화합니다. HTTP 요청을 시뮬레이션하는 데 사용할 수 있도록 변수 에 실행 중인 앱 app 에 대한 참조를 저장한다 .

 

Supertest의 request() 기능을 사용하여 HTTP 테스트를 시뮬레이션한다. 우리는 이러한 HTTP 요청이 실행 중인 Nest 앱으로 라우팅되기를 원하므로(Express 플랫폼에서 제공될 수 있음) Nest 기반의 HTTP 수신기에 대한 참조를  request() 함수에 전달한다. 따라서 request(app.getHttpServer())로 표현된 request() 호출은이제 실제 HTTP 요청을 시뮬레이션하는 메서드를 노출하는 Nest 앱에 연결된 래핑된 HTTP 서버를 전달한다. 예를 들어 request(...).get('/cats') 를 사용하면 네트워크를 통해 들어오는 것과 같은 실제 HTTP 요청 get '/cats' 동일한 Nest 앱에 대한 요청이 시작된다.

 

이 예에서는 테스트할 수 있는 하드 코딩된 값을 반환하는 CatsService  대체(test-double) 구현도 제공한다 . overrideProvider()는 이러한 대체 구현을 제공하는 데 사용된다. 마찬가지로 Nest는 각각 overrideModule() , overrideGuard(), overrideInterceptor(), overrideFilter() 및 메서드 overridePipe() 를 사용하여 모듈, 가드, 인터셉터, 필터 및 파이프를 재정의하는 메서드를 제공한다.

각 재정의 메서드( overrideModule() 제외 )는 사용자 지정 공급자에 대해 설명된 메서드를 미러링하는 3가지 다른 메서드가 있는 객체를 반환한다 .

  • useClass: 객체(공급자, 가드 등)를 재정의하기 위해 인스턴스를 제공하기 위해 인스턴스화될 클래스를 제공한다.
  • useValue: 객체를 재정의할 인스턴스를 제공한다.
  • useFactory: 객체를 재정의할 인스턴스를 반환하는 함수를 제공한다.

반면에 overrideModule() 다음과 같이 원래 모듈을 재정의할 모듈을 제공하는데 사용할 수 있는 useModule() 를 이용해 개체를 반환합니다.

const moduleRef = await Test.createTestingModule({
  imports: [AppModule],
})
  .overrideModule(CatsModule)
  .useModule(AlternateCatsModule)
  .compile();

 

각 재정의 메서드 유형은 차례로 TestingModule 인스턴스를 반환하므로 Fluent 스타일  의 다른 메서드와 연결될 수 있다 . Nest가 모듈을 인스턴스화하고 초기화하도록 하려면 이러한 체인의 끝에서 .compile() 사용해야 한다.

 

또한 때로는 테스트가 실행될 때(예: CI 서버에서) 사용자 정의 로거를 제공하고 싶을 수도 있습니다. setLogger() 메서드 를 사용하고 LoggerService 인터페이스를 충족하는 개체를 전달하여 TestModuleBuilder 에게 테스트 중에 로그하는 방법을 지시한다. (기본적으로 "오류" 로그만 콘솔에 기록됩니다).

 

컴파일된 모듈에는 다음 표에 설명된 대로 몇 가지 유용한 메서드가 있다.

createNestApplication() INestApplication지정된 모듈을 기반으로 Nest 애플리케이션(인스턴스)을 생성하고 반환합니다 .  init() 메서드를 사용하여 애플리케이션을 수동으로 초기화해야 한다.
createNestMicroservice() INestMicroservice지정된 모듈을 기반으로 Nest 마이크로서비스(인스턴스)를 생성하고 반환한다 .
get() 애플리케이션 컨텍스트에서 사용할 수 있는 컨트롤러 또는 공급자(가드, 필터 등 포함)의 정적 인스턴스를 검색한다. 모듈 참조 클래스 에서 상속된다 .
resolve() 애플리케이션 컨텍스트에서 사용할 수 있는 컨트롤러 또는 공급자(가드, 필터 등 포함)의 동적으로 생성된 범위 인스턴스(요청 또는 임시)를 검색한다. 모듈 참조 클래스 에서 상속된다 .
select() 모듈의 종속성 그래프를 탐색한다. 선택한 모듈에서 특정 인스턴스를 검색하는 데 사용할 수 있다( get() 메소드에서 엄격 모드( strict: true ) 와 함께 사용됨 ).

힌트) e2e 테스트 파일을 test디렉터리 안에 보관하자. 테스트 파일에는 .e2e-spec접미사가 있어야 한다.

 

전역적으로 등록된 인핸서 재정의

전역적으로 등록된 가드(또는 파이프, 인터셉터 또는 필터)가 있는 경우 해당 인핸서를 재정의하려면 몇 가지 추가 단계를 수행해야한다. 원래 등록을 요약하면 다음과 같다.

providers: [
  {
    provide: APP_GUARD,
    useClass: JwtAuthGuard,
  },
],

이는  APP_* 토큰을 통해 가드를 "다중" 제공자로 등록하는 것다. 여기 JwtAuthGuard 를 대체하려면 등록에서 이 슬롯의 기존 공급자를 사용해야 한다.

providers: [
  {
    provide: APP_GUARD,
    useExisting: JwtAuthGuard,
    // ^^^^^^^^ notice the use of 'useExisting' instead of 'useClass'
  },
  JwtAuthGuard,
],

힌트) Nest가 토큰 뒤에서 인스턴스화하도록 하는 대신 등록된 공급자를 참조하도록 .useClass 를 .useExisting 으로 변경한다.

 

이제 JwtAuthGuard는 TestingModule을 생성할 때 재정의될 수 있는 일반 공급자로 Nest에 표시된다.

const moduleRef = await Test.createTestingModule({
  imports: [AppModule],
})
  .overrideProvider(JwtAuthGuard)
  .useClass(MockAuthGuard)
  .compile();

이제 모든 테스트에서는 모든 요청에 ​​대해 MockAuthGuard 를 사용한다.

 

요청 범위 인스턴스 테스트 

요청 범위 공급자는 들어오는 각 요청 에 대해 고유하게 생성된다. 따라서 요청 처리가 완료된 후 인스턴스가 가비지 수집된다. 이로 인해 테스트된 요청에 대해 특별히 생성된 종속성 주입 하위 트리에 액세스할 수 없기 때문에 문제가 발생한다.

 

우리는 (위 섹션을 기반으로) 이 resolve() 메서드를 사용하여 동적으로 인스턴스화된 클래스를 검색할 수 있다는 것을 알고 있다. 또한 여기에 설명된 대로 고유한 컨텍스트 식별자를 전달하여 DI 컨테이너 하위 트리의 수명 주기를 제어할 수 있다는 것을 알고 있다. 테스트 상황에서 이를 어떻게 활용할까?

 

전략은 미리 컨텍스트 식별자를 생성하고 Nest가 이 특정 ID를 사용하여 들어오는 모든 요청에 ​​대한 하위 트리를 생성하도록 하는 것이다. 이러한 방식으로 테스트된 요청에 대해 생성된 인스턴스를 검색할 수 있다.

 

이를 수행하려면 ContextIdFactory 에서 jest.spyOn()을 사용하자.

const contextId = ContextIdFactory.create();
jest.spyOn(ContextIdFactory, 'getByRequest').mockImplementation(() => contextId);

이제 후속 요청에 대해 생성된 단일 DI 컨테이너 하위 트리에 액세스하는데 contextId 를 사용할 수 있다 .

'Backend(Framework) > NestJS 개요(공식문서 번역)' 카테고리의 다른 글

19. 수명 주기 이벤트  (0) 2023.12.03
18. Execution context  (1) 2023.12.02
17. Lazy-loading modules  (1) 2023.12.02
16. Module reference  (1) 2023.12.02
15. Circular Dependency  (1) 2023.12.02
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유