Closed6

NestJSのプロバイダー復習

RyoRyo

NestJSにおける依存関係注入DI(Dependency Injection)と独自のIoCコンテナの利用方法メモ。

https://docs.nestjs.com/providers

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

RyoRyo

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')
  })
})
RyoRyo

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`)
    })
  })
})
RyoRyo

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`)
    })
  })
})
RyoRyo

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`)
    })
  })
})
RyoRyo

依存性逆転(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')
  })
})
このスクラップは1ヶ月前にクローズされました