2022/03/23
はじめに
前回までで作成した REST API に対し,テストを導入する.
Testing ツールは Jest を用い,MySQL への接続部は,実際にアクセスして結果を得る方法で行う.(これに関して,in-memory の SQLite を用いる事でテストを行うことも可能ではある.)
何故テスト自動化をするのか
テスト自動化のメリット
ソースコードの変更に伴って過去に動作していた処理にバグを引き起こす可能性を下げることが出来る.直接変更した部分については開発者が認識できるだろうが,副次的にバグを引き起こす場合はテスト自動化でないとバグを見つけづらい.
開発者の精神衛生上よい.
CICD へ一歩近づく.
TDD (Test-Driven Development)も可能.
テスト自動化のデメリット
テストを準備すること自体に工数がかかる.そのため,短期間で開発が終了しつつ発展する余地があまりないばあいは導入しないほうが良い場合もある.
テストパターンに漏れがあると,意義が薄くなるため,頼りにしすぎるを痛い目にあう.
テストパターンやソースコードの増大によって,テストに要する時間が増大する.
テストパターンに対するデメリットの解消
バグを生み出さないように注意を払う必要がある開発においては,境界値(0
や''
)を意識して,本来入れないような入力値もテストパターンに入れることが重要になる.競技プログラミングなどでエラーになるパターンを意識することが参考になる(かも!).
セットアップ
まずは,前回と同じ方法で,DB にtest
というテーブルを作成する.
NestJS における自動テスト
NestJS におけるテストファイルは,xxx.spec.ts
という名前で(Schematics によって CLI で)自動生成される.
テスト用の AppModule を作成する
Jest 用の基本的なライブラリは NestJS 公式パッケージが公開されており,それを使う.
しかし,自分でAppModule
で設定した DB 接続などは Jest 用に設定する必要があるため,専用のものを準備する.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from '../todo/todo.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
// テスト用のテーブルを指定
database: 'test',
entities: [Todo],
// 接続毎にテーブルを初期化してくれる(テストに向いてるね!)
dropSchema: true,
synchronize: true,
}),
],
})
export class AppTestingModule {}
テストを書く!
実際に作成してみたテストが以下.解説はコード内のコメントとして記載した.
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NotFoundException } from '@nestjs/common';
import { Todo } from './todo.entity';
import { TodoService } from './todo.service';
// 上で準備したテスト用のAppModule
import { AppTestingModule } from '../mocks/app-testing.module';
// テスト用のサンプルTODOデータ
const testTodo = {
title: 'test',
description: 'test todo',
};
// テスト用のサンプルTODOデータ(2)
const updatedTodo = {
title: 'updated test',
description: 'updated test todo',
};
// describe()でテストパターンをグループ化できる
describe('TodoService', () => {
let service: TodoService;
let module: TestingModule;
// beforeAll(): describeの最初に呼ばれる
// afterAll(): describeの最後に呼ばれる
// 各`it()`/`test()`の前に呼ばれるコード
// awaitを使いたい場合はasyncでコールバックを入れる
beforeEach(async () => {
// TodoServiceをテストするため,必要なModuleをimportsに記載
// providersも同様
// NestJSにより提供される,テストモジュールを作るメソッド
module = await Test.createTestingModule({
imports: [AppTestingModule, TypeOrmModule.forFeature([Todo])],
providers: [TodoService],
}).compile();
// 準備したテストモジュールをテストで用いるためにClassメソッドに格納
service = module.get<TodoService>(TodoService);
});
// 各`it()`/`test()`の後に呼ばれるコード
afterEach(() => {
// モジュールを解放
// module.close()は非同期であり,
// このように非同期関数をコールバックの戻り値にすることでも非同期関数を扱うことが可能
return module.close();
});
// it()はtest()のエイリアス
// 文字列は,テストパターンの名前
it('should be defined', () => {
// toBeDefined()で,定義されていることを確認
expect(service).toBeDefined();
});
it('create todo', async () => {
const { id, ...created } = await service.createTodo(testTodo);
// expect(val).toBe(expected)でvalがexpectedであることを確認
expect(id).toBe(1);
// expect(obj).toEqual(expected)でobjがexpectedと同じ要素を持つことを確認(deep-equal)
expect(created).toEqual(testTodo);
});
it('get todo', async () => {
const created = await service.createTodo(testTodo);
const found = await service.findOne(created.id);
expect(found).toEqual(created);
});
it('update todo', async () => {
const created = await service.createTodo(testTodo);
const updated = await service.updateTodo({
id: created.id,
...updatedTodo,
});
expect(updated.id).toBe(created.id);
const { id, ...found } = await service.findOne(created.id);
expect(id).toBe(created.id);
expect(found).toEqual(updatedTodo);
});
it('update todo.title', async () => {
const created = await service.createTodo(testTodo);
const { id, ...updated } = await service.updateTodo({
id: created.id,
title: updatedTodo.title,
description: created.description,
});
expect(id).toBe(created.id);
expect(updated).toEqual({
title: updatedTodo.title,
description: created.description,
});
});
it('update todo.description', async () => {
const created = await service.createTodo(testTodo);
const { id, ...updated } = await service.updateTodo({
id: created.id,
title: created.title,
description: updatedTodo.description,
});
expect(id).toBe(created.id);
expect(updated).toEqual({
title: created.title,
description: updatedTodo.description,
});
});
it('delete todo', async () => {
const created = await service.createTodo(testTodo);
await expect(service.deleteTodo(created.id)).resolves.toBe(true);
// expect(func).rejects.toThrow(exception)で,非同期funcがexception例外を返すことを確認
// 非同期関数の結果はresolvesで確認可能
await expect(service.findOne(created.id)).rejects.toThrow(
NotFoundException,
);
});
});
おわりに
今回の書き方をベースにすると,多くのパターンに対応することが出来るので,いろいろ試してみてください!