GAミント至上主義

Web Monomaniacal Developer.

NestJS + TypeORMでテスト時のRepository依存エラーをちゃんとする

昨日、思考停止の力技でなんとかしまってたけど、TypeOrmModule.forRootAsyncでテスト用の接続環境を渡し、TypeOrmModule.forFeatureをしっかりやればシンプルな設定で動いたのでメモ。

NestJS + TypeORM でテスト時のRepository依存エラーを力技でなんとかする - GAミント至上主義

Test.createTestingModuleのimporsにテスト接続情報を読ませるTypeOrmModule.forRootAsync()と、必要な全Entityを指定したTypeOrmModule.forFeature()の両方渡すってのがポイント?書いてて、forRootAsyncだけでなんとかできるかもという気もした。

useFactory の nameのところは下記
NestJSでテスト用DB接続を使ったテスト実行時のNest could not find testConnection element エラーの対処 - GAミント至上主義

service 単位のテストの時はcreateTestingModule使わなくていいかなと思ったけど、これで解決できたので使ったほうがいいなってなった。
TypeOrmModule.forFeatureに渡すEntityの配列は、数が多いと毎回書くの大変なのでどこかに書いてexportして共通化した方がよさそう。

import { Test, TestingModule } from '@nestjs/testing'
import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm'
import { INestApplication } from '@nestjs/common'
import { getConnectionOptions, Repository } from 'typeorm'
//省略
describe('UsersService', () => {
  let usersService: UsersService
  let userRepository: Repository<User>
  let userProfileRepository: Repository<UserProfile>
  let app: INestApplication

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UsersService],
      imports: [
        TypeOrmModule.forRootAsync({
          useFactory: async () => {
            const options = await getConnectionOptions('test')
            return {
              ...options,
              name: null,
            }
          },
        }),
        TypeOrmModule.forFeature([
          User,
          UserProfile,
          ExperiencedCompany,
          // 省略
        ]),
      ],
    }).compile()
    app = module.createNestApplication()
    usersService = app.get<UsersService>(UsersService)
    userRepository = app.get<Repository<User>>(getRepositoryToken(User))
    userProfileRepository = app.get<Repository<UserProfile>>(
      getRepositoryToken(UserProfile),
    )
  })

レポジトリを使いたいときは こんな感じで取り出せた。

userRepository = app.get<Repository<User>>(getRepositoryToken(User))

NestJSでテスト用DB接続を使ったテスト実行時のNest could not find testConnection element エラーの対処

NestJS + TypeORMの環境で、e2eテスト実行時に、テスト用接続情報を下記のように使用したところエラーが出た。
ググっても記事出てこないのでメモ。

app.e2e-spec.ts
```
import { getConnectionOptions } from 'typeorm'

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

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
UsersModule,
TypeOrmModule.forRootAsync({
useFactory: async () => {
return await getConnectionOptions('test')
},
}),
],
exports: [TypeOrmModule],
providers: [],
}).compile()
app = module.createNestApplication()
await app.init()
})
//省略
```

実行時のエラー
```
$ yarn test:e2e
yarn run v1.22.10
$ jest --config ./test/jest-e2e.json
FAIL test/app.e2e-spec.ts (8.139 s)


● Test suite failed to run

Nest could not find testConnection element (this provider does not exist in the current context)

at InstanceLinksHost.get (../node_modules/@nestjs/core/injector/instance-links-host.js:15:19)
at Object.find (../node_modules/@nestjs/core/injector/module-ref.js:38:55)
at Object.get (../node_modules/@nestjs/core/injector/module.js:345:28)
at TypeOrmCoreModule. (../node_modules/@nestjs/typeorm/dist/typeorm-core.module.js:96:47)
at ../node_modules/@nestjs/typeorm/dist/typeorm-core.module.js:20:71
at Object..__awaiter (../node_modules/@nestjs/typeorm/dist/typeorm-core.module.js:16:12)
at TypeOrmCoreModule.onApplicationShutdown (../node_modules/@nestjs/typeorm/dist/typeorm-core.module.js:92:16)
at Object.callAppShutdownHook (../node_modules/@nestjs/core/hooks/on-app-shutdown.hook.js:51:35)
```


下記のようなormconfig.tsを使用してdefaultとtestを書いており、getConnectionOptionsでtestの方を使用していた。
```
// https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md
module.exports = [
{
name: 'default',
type: 'mysql',
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
logging: process.env.DB_LOGGING,
synchronize: process.env.DB_SYNCHRONIZE,
migrationsRun: process.env.DB_MIGRATIONS_RUN,
entities: ['src/**/*.entity.ts'],
migrations: ['src/migration/*.ts'],
migrationsTableName: 'migrations',
cli: {
migrationsDir: 'src/migration',
},
},
{
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,
},
]
```

エラーのtestConnection からわかるように自動的に生成されるはずのものっぽいのが見つからないということで、nameを消してみたところ解決できた。
nameは必要だけどいろいろ邪魔になるっぽい。
getConnectionOptionsの返り値は全部realonlyなので新しいオブジェクトでnameをnullにして返した。

```
useFactory: async () => {
const options = await getConnectionOptions('test')
return {
...options,
// いろいろエラーの原因になるっぽいので消す
name: null,
}
},
```

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()
  })