皆さんはじめまして、アットウェアの小松です。
QAという業務にはあまり馴染みが無い私ですが、今回はユニットテストの品質について、書かせていただきます。
ユニットテスト自体は多かれ少なかれ、「ソフトウェアの品質を向上するために必要なもの」として一般的に認識されているものと思います。
では、「ユニットテストの品質」は意識されていますでしょうか。
当たり前ですが、ユニットテストは「書けばいいもの」ではありません(もちろん、書かないより書いたほうが100倍良いです)。
品質の高いユニットテストはメンテナンスがしやすく、アサーションエラーが発生したときに短時間で原因箇所を特定できます。
品質の高いユニットテストとはどういったものでしょうか。以下にそのポイントを挙げます。
- テスト対象のスコープが適切である
- 適切にモッキングされている
- テスト対象のコードと同様に、適切に構造化・共通化されている
1. テスト対象のスコープが適切である
一つのテストコード(テストメソッド)では、極力小さいスコープのテストを行いましょう。
たとえば、あるモジュールにメソッド a, b, c があり、互いに独立したメソッドであるとします。このモジュールに対応するテストモジュールでは、メソッド a, b, c に対応しているテストメソッドが独立して存在するのが良いテストコードです。
そうすることで、テストメソッドの役割が明確になり、またテストコードもより簡潔になります。
複数のメソッドを一つのテストメソッドで束ねてテストしてしまうと、テスト対象のメソッド(前述のケースの場合、 a, b, c)を改修する場合に、都度同じテストメソッドを改修する必要が生じてしまいます。
独立したメソッドを束ねてテストするメリットは一切ありません。
また、テスト対象メソッドが入力や状態によって条件分岐を行う場合、テストメソッドは条件ごとに分けましょう。
こうすることで、メンテナンス性はもちろんのこと、アサーションエラーが発生した場合、「どの条件でアサーションエラーが発生したのか」が明確になります。
具体的に、Javaのテストコードでの例を以下に挙げます。
@Test
public void testLogin_利用者有効_パスワード一致() throws Exception {
// 利用者が有効で、パスワードが一致する場合のテストコード
}
@Test
public void testLogin_利用者有効_パスワード不一致() throws Exception {
// 利用者が有効で、パスワードが不一致の場合のテストコード
}
@Test
public void testLogin_利用者無効() throws Exception {
// 利用者が無効の場合のテストコード
}
2. 適切にモッキングされている
モッキングとは、モック(mock)やスタブ(stub)と呼ばれる、偽物の実装を使うことです。
モッキングすることで、テスト対象のスコープをより単純化でき、テストコードが書きやすくなります。
プレゼンテーション層、ビジネスロジック層、データアクセス層の3層モデルの場合、それぞれの層は片方向(プレゼンテーション層→ビジネスロジック層、ビジネスロジック層→データアクセス層)で結合しています。
単純にテストコードを書こうとすると、最悪の場合でプレゼンテーション層のテストコードでデータアクセス層までテストすることになります。3層それぞれでテストコードを書くと、データアクセス層を3重に、ビジネスロジック層を2重にテストすることになり、大きな無駄が生じます。
また、層をまたがってテストをすることは、他の層の実装変更の影響を受けるため、テストコードの改修コストが大きくなります。たとえば、データアクセス層の実装を修正することで、最悪の場合でビジネスロジック層とUI層のテストコードまで改修する必要が生じてしまうのです。
プレゼンテーション層のテストコードではビジネスロジック層をモッキングし、ビジネスロジック層のテストコードではデータアクセス層をモッキングします。そうすることで、他層の実装変更の影響を受けにくいテストコードになります。
3. テスト対象のコードと同様に、適切に構造化・共通化されている
言うまでもないことですが、テスト対象コードと同様に、テストコードも品質が高いものが要求されます。
テストコードの記述に手を抜くと、テストコードが冗長・複雑になります。
テスト対象コードはもちろんのこと、テストコードも構造化・共通化しましょう。
また、リファクタリング時には、テストコードもリファクタリングしましょう。
値の入出力にプリミティブ型ではなく独自のオブジェクトを使用する場合にはどうしてもアサーションコードが多くなりがちです。
それらをテストコード用の独自アサーションメソッドとして切り出すことでずいぶんとテストコードは見やすくなります。
複数のテストクラスで共通の処理(たとえば、データアクセス層のテストでの共通の事前準備)があれば、共通処理を行う親テストクラスを作成しましょう。
また、モッキングやスタブの準備など、各テストメソッドで毎回必要になる事前準備や事後処理は setUp/tearDown(JUnitの場合)にまとめましょう。
setUp メソッドは各テストメソッドの実行前に呼び出され、 tearDown メソッドは各テストメソッドの実行後に呼び出されます。
たとえば、データアクセス層のユニットテストで、テーブルを最初に初期化する場合は、初期化処理を setUp メソッドに記述します。
まとめ
ソフトウェアは生き物とよく言われますが、ユニットテストもまたその生き物の一部です。
ユニットテストを活かすも殺すも、開発者の力量次第です。
良いユニットテストが品質の良いソフトウェアを生むとは限りませんが、粗雑なユニットテストが品質の良いソフトウェアを生むことはないはずです。
品質の良いソフトウェアを生み出すため、品質の良いユニットテストを書くことを意識しましょう。