NestJSのプロバイダー復習
NestJSにおける依存関係注入DI(Dependency Injection)と独自のIoCコンテナの利用方法メモ。
Providers are a fundamental concept in Nest. Many of the basic Nest classes may be treated as a provider – services, repositories, factories, helpers, and so on
Cats App
確認用にサンプルのCats App
の作成。
特定の文字列を返すだけのAPIで実装。
Service
cats.service.ts
import { Injectable } from '@nestjs/common'
@Injectable()
export class CatsService {
name(): string {
return `Luna`
}
}
Controller
cats.controller.ts
import { Injectable } from '@nestjs/common'
@Injectable()
export class CatsService {
name(): string {
return `Luna`
}
}
Module
cats.module.ts
import { Module } from '@nestjs/common'
import { CatsController } from './cats.controller'
import { CatsService } from './cats.service'
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
動作テスト
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, Module } from '@nestjs/common'
import * as request from 'supertest'
import { CatsModule } from './cats.module'
describe('Cats Controller (e2e)', () => {
let app: INestApplication
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [CatsModule],
}).compile()
app = moduleFixture.createNestApplication()
await app.init()
})
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect('Luna')
})
})
Class providers: useClass
The useClass syntax allows you to dynamically determine a class that a token should resolve to. For example, suppose we have an abstract (or default) ConfigService class. Depending on the current environment, we want Nest to provide a different implementation of the configuration service.
標準的なプロバイダでこの場合の短縮系がproviders: [CatsService]
に該当する。
公式のサンプルでは環境変数(env)に応じで動的に構成の異なるサービスの実装を提供している。
テスト
ここではテスト環境でMockサービスにクラスをオーバーライドする。
import { Test } from '@nestjs/testing'
import { CatsController } from './cats.controller'
import { CatsService } from './cats.service'
class MockCatService {
name(): string {
return `Leo`
}
}
describe('Class Provider', () => {
let mockCatsController: CatsController
let mockCatsService: CatsService
beforeEach(async () => {
const mockModuleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [
{
provide: CatsService,
useClass: MockCatService,
},
],
}).compile()
mockCatsService = mockModuleRef.get<CatsService>(CatsService)
mockCatsController = mockModuleRef.get<CatsController>(CatsController)
})
describe('call name', () => {
it('should return an string of mock cats name', async () => {
expect(await mockCatsController.name()).toBe(`Leo`)
})
})
})
Value providers: useValue
The useValue syntax is useful for injecting a constant value, putting an external library into the Nest container, or replacing a real implementation with a mock object.
公式のサンプルだけですと、useClass
とほとんど似たような印象を受けるところです。
定数や直接的にevnから値を受け取るのではなくuseValueを利用する場合などに利用するケースが多そうです。
app.module.ts
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: 'PORT',
useValue: 3000,
},
],
})
export class AppModule {}
テスト
cats.controller.ts
に@Inject('CAT_COLOR')
を追加し受け取りの確認をします。
import { Controller, Get, Inject, Optional } from '@nestjs/common'
import { CatsService } from './cats.service'
@Controller('cats')
export class CatsController {
constructor(
private readonly catsService: CatsService,
@Optional() @Inject('CAT_COLOR') private readonly cat_color: string // 追加
) {}
@Get()
async name(): Promise<string> {
return this.catsService.name()
}
// 追加
@Get('/color')
async color(): Promise<string> {
return this.cat_color
}
}
テストコードでトークンに値を設定します。
import { Test } from '@nestjs/testing'
import { CatsController } from './cats.controller'
import { CatsService } from './cats.service'
describe('Value Provider', () => {
let catsController: CatsController
let catsService: CatsService
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [
CatsService,
{
provide: 'CAT_COLOR',
useValue: `White`,
},
],
}).compile()
catsService = moduleRef.get<CatsService>(CatsService)
catsController = moduleRef.get<CatsController>(CatsController)
})
describe('call color', () => {
it('should return an string of mock cats name', async () => {
expect(await catsController.color()).toBe(`White`)
})
})
})
Factory providers: useFactory
The useFactory syntax allows for creating providers dynamically. The actual provider will be supplied by the value returned from a factory function. The factory function can be as simple or complex as needed.
初期オプションが必要な外部ライブラリを利用する際に利用するケースが多そうです。
テスト
簡単にuseFactoryでの動作確認。
import { Test } from '@nestjs/testing'
import { CatsController } from './cats.controller'
import { CatsService } from './cats.service'
describe('Factory Provider', () => {
let catsController: CatsController
let catsService: CatsService
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [
{
provide: CatsService,
useFactory: () => {
return new CatsService()
},
},
],
}).compile()
catsService = moduleRef.get<CatsService>(CatsService)
catsController = moduleRef.get<CatsController>(CatsController)
})
describe('call name', () => {
it('should return an string of mock cats name', async () => {
expect(await catsController.name()).toBe(`Luna`)
})
})
})
依存性逆転(IoC)
ご存知の通りJavaScriptではインターフェースをサポートしていないため、
TypeScriptにおけるinterfaceは型検査時にのみ利用されトランスパイル後は消失してしまいます。
abstract修飾子による代用手法もありますが、実装詳細を含むことができてしまうため利用するケースは低そうです。
NestJSでは非クラスベースのプロバイダートークンを利用してDIすることが可能なため
依存関係を抽象化したインターフェースに落とし込むことが可能です。
Interface
cats.service.interface.ts
export interface ICatsService {
name(): string
}
export const ICatsService = 'ICatsService'
Service
cats.service.ts
import { Injectable } from '@nestjs/common'
import { ICatsService } from './cats.service.interface'
@Injectable()
export class CatsService implements ICatsService {
name(): string {
return `Luna`
}
}
Controller
cats.controller.ts
import { Controller, Get, Inject } from '@nestjs/common'
import { ICatsService } from './cats.service.interface'
@Controller('cats')
export class CatsController {
constructor(@Inject(ICatsService) private readonly catsService: ICatsService) {}
@Get()
async name(): Promise<string> {
return this.catsService.name()
}
}
Module
cats.module.ts
import { Module } from '@nestjs/common'
import { CatsController } from './cats.controller'
import { CatsService } from './cats.service'
import { ICatsService } from './cats.service.interface'
@Module({
controllers: [CatsController],
providers: [
{
provide: ICatsService,
useClass: CatsService,
},
],
})
export class CatsModule {}
テスト
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, Module } from '@nestjs/common'
import * as request from 'supertest'
import { CatsModule } from './cats.module'
describe('Interface Provider Controller (e2e)', () => {
let app: INestApplication
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [CatsModule],
}).compile()
app = moduleFixture.createNestApplication()
await app.init()
})
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect('Luna')
})
})