GAミント至上主義

Web Monomaniacal Developer.

NestJS + TypeORM でテスト時のRepository依存エラーを力技でなんとかする

2021/9/30 ちゃんと解決できたのでこちらの記事参照

uyamazak.hatenablog.com


以下、特に役に立つことはなさそう

NestJS + TypeORMでJestのテスト時、@InjectRepositoryを使った部分の依存関係がなかなか解決できなかった。
そのため@InjectRepositoryに頼らず、依存をすべて手動で注入したところ、なんとか動かすことができたのでメモ。

前提

カラム数、関連レーブル数ともに非常に多いEntityだったので自動でなんとかしたかったけど無理だった・・・。

まだNestJS + TypeOrm の経験も浅く、もっといい方法もありそう。

NestJSにとってはTypeORMはオプションの一つでしかないので、公式ドキュメントだとちょっと足りないことがありがちな印象。

e2eでコントローラーだけでなく、サービス、レポジトリ、エンティティの全体のテストがしたかったので、モックではなく本物のレポジトリと、テスト用に用意したDBを使いたかった。

エラーなどでぐぐりのかなりのStackOverflowを見たけどどれもだめで、結局手で注入する方法にたどり着いた。

jestjs - NestJs - Jest - Testing: ConnectionNotFoundError: Connection "default" was not found - Stack Overflow

以下だめだったやつ。

users.controller.spec.ts

import * as request from 'supertest'
import { INestApplication } from '@nestjs/common'
import { Test, TestingModule } from '@nestjs/testing'
import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
import {
  Connection,
  Repository,
  getConnectionOptions,
  createConnection,
} from 'typeorm'
import { User } from '../entity'

// 省略
describe('UsersController', () => {
  let app: INestApplication
  let connection: Connection

  beforeAll(async () => {
    const testOptions = await getConnectionOptions('test')
    connection = await createConnection(testOptions)
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [UsersService],
      imports: [TypeOrmModule.forRoot(testOptions)],
    }).compile()
    app = module.createNestApplication()
    await app.init()
  })


下記のエラーで出て、imports、providersなどをいろいろ変えたりしたけどだめ。このエラー100回ぐらいみた。

    Nest can't resolve dependencies of the UsersService (?, UserProfileRepository, ExperiencedCompanyRepository, CityMasterRepository, LanguageMasterRepository, BusinessTripMasterRepository, GraduationStatesMasterRepository, RetirementAgeMasterRepository, SchoolTypeMasterRepository, SalaryTypeMasterRepository, WorkingDaysOptionMasterRepository, WorkingHoursOptionMasterRepository, EmploymentTypeMasterRepository, WorkLocationMasterRepository, ExperiencedOccupationRepository, LanguageSkillRepository, QualificationMasterRepository, SkillMasterRepository, OccupationMasterRepository, LanguageLevelMasterRepository, DesiredSalaryOptionRepository, SalaryExpectationMasterRepository). Please make sure that the argument UserRepository at index [0] is available in the RootTestModule context.

    Potential solutions:
    - If UserRepository is a provider, is it part of the current RootTestModule?
    - If UserRepository is exported from a separate @Module, is that module imported within RootTestModule?
      @Module({
        imports: [ /* the Module containing UserRepository */ ]
      })

レポジトリはTypeOrmModuleで作られると思ったのでTypeOrmModule周りもいろいろ試したけど変わらず。

だめだったやつ2

const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [UsersService],
      imports: [
        TypeOrmModule.forRoot({
          ...testOptions,
          autoLoadEntities: true,
        }),
        TypeOrmModule.forFeature([User, UserProfile, //省略], 'test'),
      ],
    }).compile()

UsersControllerはドキュメントにもありがちなこんな感じので、

import {
  Controller,
  Body,
  Post,
  Inject,
} from '@nestjs/common'
import { CreateUserDTO } from '../dto'
import { UsersService } from './users.service'

@Controller('users')
export class UsersController {
  constructor(
    @Inject(UsersService)
    private usersService: UsersService,
  ) {}

  @Post()
  async create(@Body() createUserDTO: CreateUserDTO) {
    const createdUser = await this.usersService.create(createUserDTO)
    return { id: createdUser.id }
  }
}

そこで使ってるUsersServiceが非常に大きいuserテーブルのおかげでconstructorがすごいことになってこんな感じ

// 省略
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    @InjectRepository(UserProfile)
    private userProfilesRepository: Repository<UserProfile>,
    @InjectRepository(ExperiencedCompany)
    private experiencedCompanyRepository: Repository<ExperiencedCompany>,
    @InjectRepository(CityMaster)
    private cityMasterRepository: Repository<CityMaster>,
    // 省略

UsersServiceを接続名指定で初期化する

他のテストでもこのUsersServiceは利用するので共通化のために下記のような関数をつくった。
依存しているレポジトリをすべてgetRepository()でつくり、サービスをnewして返すだけ。

getRepositoryの第2引数で接続名(今回はtest)を指定できるようにしたけど、 @InjectConnectionとかあるから他にやりようがあるかも。

import { getRepository } from 'typeorm'
// 省略
export const initUserServiceWithConnectiion = async (
  connectionName: string,
) => {
  const userRepository = getRepository(User, connectionName)
  const userProfileRepository = getRepository(UserProfile, connectionName)
  const experiencedCompanyRepository = getRepository(
    ExperiencedCompany,
    connectionName,
  )
  //  省略
  return new UsersService(
    userRepository,
    userProfileRepository,
    experiencedCompanyRepository,
    //  省略
  )
}

テストDB設定

まだ開発中でローカル環境でしか動いてないけどormconfig.ts でこんな感じ。
nameがdefaultとtestで2つ用意する。省略するけどテスト用DBはdocker composeでtmpfs使ってインメモリなやつ立てておく。
今回はtestだけつかう。

module.exports = [
  {
    name: 'default',
    type: 'mysql',
    // 省略
  },
  {
    name: 'test',
    type: 'mysql',
    host: process.env.TEST_DB_HOST,
    port: process.env.TEST_DB_PORT,
    username: process.env.TEST_DB_USERNAME,
    password: process.env.TEST_DB_PASSWORD,
    database: process.env.TEST_DB_DATABASE,
    synchronize: false,
    dropSchema: true,
    entities: ['src/**/*.entity.ts'],
    migrationsRun: true,
    migrations: ['src/migration_test/*.ts'],
    logging: false,
  },
]

テスト側の修正

上記のメソッドで作ったUsersServiceをProviderで渡すことによりUsersControllerでも使えるようになり、エラーなく実行することができた。
appとconnection は最後に終了できるようにメンバーで宣言しておく。
全部手動でレポジトリを作ったので、TypeOrmModule.forRoot()はいらなくなる + 2回connection を作ってしまうのでエラーになるので削除する必要があった。

describe('UsersController', () => {
  let app: INestApplication
  let connection: Connection

  beforeAll(async () => {
    const testOptions = await getConnectionOptions('test')
    connection = await createConnection(testOptions)
    const usersService = await initUserServiceWithConnectiion('test')
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [{ provide: UsersService, useValue: usersService }],
    }).compile()
    app = module.createNestApplication()
    await app.init()
  })

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