概要
- 統合テストとは何か
- 統合テストにおけるプロセス外依存の扱い方について
- 統合テストの実装方法
8.1 統合(integration)テストとは?
統合テストとは?
単体テストの性質を1つでも損なっていたら統合テストに分類される。
- 単体テスト
- 1単位の振る舞い(a unit of behavior)検証すること
- 実行時間が短いこと
- 他のテスト・ケースから隔離された状態で実行されること
通常、単体テストはドメイン・モデルを検証し、統合テストはドメイン・モデルとプロセス外依存を結びつけるコードを検証する。
※ コントローラのテストも全てのプロセス外依存をモック化し、他のテストケースから隔離された状態にすれば単体テストになり得る
統合テストの保守コストは単体テストよりも高い
プロジェクトを継続的に運用する上で重要なことの一つに、単体テストと統合テストのテスト・ケースの数をうまく調整する、というものがある。
ほとんどのアプリケーションは検証する内容の大半を単体テストに持たせている状態が良い。
- テスト・スイート全体の保守コストを少なくなる
- ビジネス・シナリオごとに1件か2件の包括的な統合テストを行うことで、システム全体が正しく機能することに自信を持てるようになる
しかし、複雑さのないアプリケーションの場合はドメイン・モデルに分類されるコードがほぼ含まれないため、統合テストと単体テストのテスト・ケースの数はほぼ同じになることもある。
8.2 どのようなプロセス外依存をモックに置き換えるべきか?
2種類のプロセス外依存
全てのプロセス外依存は大きく分けて次の2つに分類できる。
-
管理下にある依存(managed dependency)
- テスト対象のアプリケーションが好きなようにすることができるプロセス外依存
- データベースに対して外部アプリケーションがアクセスする際は提供するAPIを利用する
-
管理下にない依存(unmanaged dependency)
- テスト対象のアプリケーションが好きなようにすることができないプロセス外依存
- 他のアプリケーションに見える副作用を発生させる(例:メール・サービス、メッセージ・バス)
管理下にある依存に対しては実際のインスタンスを使用し、管理下にない依存に対してはモックを使用する。
管理下にある依存は実際のインスタンスを使用すれば最終的にどのような状態になるのかを確認することが可能になるだけではなく、DBのリファクタリング(カラムの名前の変更や他のデータベースへの移行)を行いやすくなる。
Q: テーブルの一部を別アプリケーションと共有している場合はどうする?
このような使い方をするべきではないが、管理下にない依存を持つテーブル(= 共有しているテーブル)だけをモック化することで、管理下にある依存を持つテーブルの最終的な状態を検証することができる。
Q: 統合テストで実際のDBを使えない場合はどうする?
- DBをモックに置き換えてテストする?
- リファクタリングへの耐性が失われる
- 退行に対する保護が失われる
- 全てのプロセス外依存をモック化した場合、最終的な状態を検証することができない(単体テスト以上の効果を発揮しない)
実際のデータベースを使って検証ができないのであれば、そのことに関する統合テストを作成せず、ドメイン・モデルの単体テストを作成することだけに専念する方が良い。
8.3 どのように統合(integration)テストを行うのか?
7章で見たユーザー管理システムに対して統合テストを実装する。
// ユーザー管理システムのコントローラ
class UserController {
private readonly _database: Database = new Database();
private readonly _messageBus: MessageBus = new MessageBus();
changeEmail(userId: number, newEmail: string): string {
const userData = this._database.getUserById(userId);
const user: User = UserFactory.craete(userData);
const error: string = user.canChangeEmail();
if (error != null) return error;
const companyData = this._database.getCompany();
const company: Company = CompanyFactory.create(companyData);
user.changeEmail(newEmail, company);
this._database.saveCompany(company);
this._database.saveUser(user);
user.emailChangedEvents.each((ev: EmailChangedEvent) => {
this._messageBus.sendEmailChangedMessage(ev.userId, ev.newEmail);
});
return 'OK';
}
}
テストするシナリオ
どんなテストケースを用意するのが良いか?
- 全てのプロセス外依存をを経由してビジネス・シナリオを正常に終わらせる実行経路であるテストケース
- メールアドレスを従業員のものから非従業員のものに変えることによって、ユーザーと会社の両方の情報が更新される
- 単体テストでは検証できなかった全ての異常ケースををそれぞれ検証するテストケース
- 早期失敗(Fail Fast)しているため、統合テストで検証する必要はない
以上の理由から、「メールアドレスを従業員のものから非従業員のものに変える」 統合テストを実装する。
モックに置き換えるかどうかの分類
DBは管理下にある依存のため直接アクセス、メッセージバスは管理下にない依存のためモックを使用する。
統合テストの作成
describe('UserController', () => {
describe('changeEmail', () => {
test('ChangingEmailFromCorporateToNonCorporate', () => {
/**
* 準備(Arrange)
*/
const db = new Database(connectionString);
// 検証で使うユーザーと会社の情報をDBに保存する
const user: User = createUser('user@mycorp.com', UserType.Employee, db);
createCompany('mycorp.com', 1, db);
const messageBusMock = new Mock<IMessageBus>();
const sut = new UserController(db, messageBusMock.object);
/**
* 実行(Act)
*/
const result = sut.changeEmail(user.userId, 'new@gmail.com');
/**
* 確認(Assert)
*/
expect(result).toEqual('OK');
// ユーザーの状態を確認する
const userData = db.getUserById(user.userId);
const userFromDb: User = UserFactory.create(userData);
expect(userFromDb.email).toEqual('new@gmail.com');
expect(userFromDb.type).toEqual(UserType.Customer);
// 会社の状態を確認する
const companyData = db.getCompany();
const conpanyFromDb: Company = CompanyFactory.create(companyData);
expect(conpanyFromDb.numberOfEmployees).toEqual(0);
// モックへの呼び出しを確認する
expect(messageBusMock).toHaveBeenCalled();
});
});
const createCompany = (
domainName: string,
numberOfEmployees: number,
db: Database,
): Company => {
const company: Company = new Company(domainName, numberOfEmployees);
db.saveCompany(company);
return company;
};
const createUser = (
email: string,
type: UserType,
db: Database,
): User => {
const user: User = new User(0, email, type, false);
db.saveUser(user);
return user;
};
});
まとめ
今回は統合テストとは何かというところから、統合テストで使用するプロセス外依存の扱い方、実際に実装する際の流れについてまとめました。統合テストは処理後の状態を検証するのにとても役立つテスト手法なので、単体テストとの割を考慮しつつ開発に組み込んでいけばとても有効はテストになると感じました。



