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