ユニットテスト。ソフトウェアテストの中でも、最小単位(関数やクラス)に対して行うテストです。
今では大抵の場合、ユニットテスト=フレームワークを使った自動テスト、を意味します。
そんなユニットテストで、気を付けるべきことは何でしょうか?
そのキーワードは、2つの”FIRST”です。
まずは実際のユニットテスト例
まず、テスティングフレームワークを使った、実装例を見ておきましょう。
ユニットテストの基本構成
言語はJava、テスティングフレームワークにJUnitを使ったサンプルです。
テスト対象のコード。素数を判定するシンプルなクラスです。
package com.example.project;
public class PrimeNumberChecker {
// 素数を判定するメソッド
public boolean isPrime(int number) {
if (number <= 1) {
return false;
}
// 2からnumber-1までの数で割り切れるかどうかを調べる
for (int i = 2; i < number; i++) {
if (number % i == 0) {
return false;
}
}
return true;
}
}
テストコードは、こんな感じになります。
package com.example.project;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class PrimeNumberCheckerTest {
// 1は素数でない
@Test
public void testOneIsNotPrime() {
PrimeNumberChecker checker = new PrimeNumberChecker();
Assertions.assertFalse(checker.isPrime(1));
}
// 2は素数
@Test
public void testTwoIsPrime() {
PrimeNumberChecker checker = new PrimeNumberChecker();
Assertions.assertTrue(checker.isPrime(2));
}
// 3は素数
@Test
public void testThreeIsPrime() {
PrimeNumberChecker checker = new PrimeNumberChecker();
Assertions.assertTrue(checker.isPrime(3));
}
// 4は素数でない
@Test
public void testFourIsNotPrime() {
PrimeNumberChecker checker = new PrimeNumberChecker();
Assertions.assertFalse(checker.isPrime(4));
}
}
特徴はこんなところ。
- mainがない テストメソッドを追加するだけで、フレームワークが自動的にテストを実行してくれます。
- 自動で結果を判定する フレームワークが持つassertなどの判定操作で、OK/NGが判定できます。
テストメソッドを順次呼び出し、判定がすべてOKなら、テストは全パス、ということになります。
PrimeNumberCheckerTest > testOneIsNotPrime() PASSED
PrimeNumberCheckerTest > testTwoIsPrime() PASSED
PrimeNumberCheckerTest > testThreeIsPrime() PASSED
PrimeNumberCheckerTest > testFourIsNotPrime() PASSED
さまざまな言語のユニットテスト
多くの言語で、テスティングフレームワークを利用することができます。
たいていの言語では、ユニットテストのライブラリを導入して利用します。
また、GoやRustといった最近の言語なら、言語仕様自体にテスト機能が組み込まれています。標準装備であるため、余計な手間いらずで使いやすくなっています。
最もよく使われているテスティングフレームワークの例です。
言語 | フレームワーク | 補足 |
---|---|---|
C++ | Google Test | Google製フレームワーク |
Java | JUnit | Javaで最も普及 |
C# | xUnit | .NET Coreに最適化 |
Python | pytest | Pythonで最も人気 |
JavaScript | Jest | React等にも適用できる |
PHP | PHPUnit | PHPで最も広く普及 |
Ruby | RSpec | Rubyで最も普及 |
Kotlin | JUnit | Javaと同じフレームワーク |
Swift | XCTest | Apple公式フレームワーク |
Go | testing | Goの標準ライブラリ |
Rust | Built-in Test Framework | Rust標準のテスト機能 |
書き方の作法はそれぞれのフレームワークで異なりますが、mainがない、判定操作がある、といった特徴はどれも同じです。
ユニットテストで意識する、”FIRST”原則
ユニットテストの具体的な組み方は、それぞれのフレームワークの解説記事を見るとして、
ここからは、ユニットテストで意識しておく、大事な2つの“FIRST”の話です。
そのひとつめ、FIRST原則。ユニットテストを組む時に守っておくべき、5つの原則です。
ユニットテストで守るべき、5つの原則
- Fast(高速)
- Isolate(独立)
- Repeatable(繰り返し可能)
- Self-Validate(自己検証)
- Timely(適切なタイミング)
それぞれ見ていきましょう。
Fast:高速でテストができること
ユニットテストは、一度組んだら、プログラム修正のたび何度も何度も実行します。
最初はテスト数も少ないから、すぐ終わります。
だけど、だんだんテスト数が増えてくると、一回流し終えるだけでも時間がかかるようになってきます。
高速で終わるようにテストを作りましょう。
- 複雑なアルゴリズム計算など、どうしても時間がかかるなら、他のテストと分離できるようにしておきます。
- サブモジュールで時間がかかるなら、スタブやモックで差し替えるといった工夫もしておきましょう。
Isolate:他のテストと独立であること
テストは、継続的に追加されたり見直されたりします。なので、テストが将来どのような順番で動くのか、保証がありません。
例えば、あるテストでファイルに書き込み、次のテストでそのファイルを読み込む、なんてしても、その順番に動くとは限らない。
テストは互いに独立して動作する必要があります。そのテストを1つだけ実行しても、全部実行しても、順番を変えても、同じ結果を出すようにしておきます。
リソース/モック/スタブなどの準備は、テストケースごとに準備するのがよいですね。
Repeatable:常に同じ結果となること
テストは動作結果を保証するものです。なので、何回実行しても、常に同じ結果を返すようにします。
この妨げとなるのが、タイミングにより変動する情報を扱うもの。
- 現在時刻
- データベースのデータ
- ランダムデータ
こういったデータに直絶依存する処理は、テストのたびに結果が変わってしまいます。
依存性注入といったテクニックで変動情報を分離しておき、その部分をモック化するなどが必要です。
Self-Validate:自動で判定できること
テストは、自動的に成功か失敗かを判断するようにしておきます。
プリント結果を目視で判断する、なんてテストを組み込んではダメです。自動でやる意味ない。
大量のテストを流したときに、すべて正常か、もしくはどこが異常か。フレームワークの判定機構で、自動的に判定できるようにしておきましょう。
Timely:いつでもテストできること
いま書いているコードを、直したら、すぐにテストです。こういう環境を整えておきます。
最近では、CI/CD(Continuous Integration(継続的インテグレーション)Continuous Delivery(継続的デリバリー)の考え方も普及しています。
ソフトウェアの変更を常にテストし、自動で本番環境にリリース可能な状態にする。こうして常に品質水準を保つのです。
FIRST原則が示すもの
FIRST原則に則る、ということは、率直に言えばこういうこと。
ユニットテストは
- いつでも
- すぐに
- 何回でも
できるようにしておく
いつでも、すぐに、何回でもテストすることで、プログラムの品質を高水準で保つのです。
テストを、”FIRST” (最初)からやろう
ユニットテストで大事な2つの”FIRST”。ふたつめはこれです。
コーディングを開始すると同時に、テストもコーディングする。(Test First)
最初からテストを組み込む
一般的には、コーディング→テストの順に、開発工程が進みます。
でも、コーディングが完了してから、ユニットテストを実装するのは、遅すぎです。
後付けでユニットテストを実装しようとすると、ものすごく手間がかかります。
ひとつめのクラスや関数のインターフェースを作ったら、すぐさまテストもコーディング。これくらい早めにテストを組みましょう。
テストファーストによる隠れたメリット
まずテストを組む。このテストファーストを意識すると、隠れたメリットも実感します。
1.インターフェースが洗練される
モジュールのテストを実装するとき、モジュールを使う側の気持ち、に立つことができます。すると、
- このメソッド名だと分かりにくいかな?
- こういう引数は余計かな?
みたいなことが、よく見えるようになります。
使う人が、分かりやすく使いやすいように、インターフェースが洗練されていくわけです。
2.モジュール構成が洗練される
ユニットテスト環境は、本番環境と別に作ります。
たくさんのテストをするので、できるだけテスト環境を簡単に作りたい。
そんなときに、複雑な依存関係があったりすると、ビルド手順が面倒。余計な依存関係を外したりといった工夫をしだします。
その過程で、モジュールの疎結合・高凝集が進むというわけです。
3.自信を持って修正できる
プログラムが大規模になってくると、ある修正が、ほかに影響を与える可能性が出てきます。
プログラムの修正は、不安との戦いです。
そんなときに、すかさずユニットテスト。
その場でテストが通ることを確認できれば、自信をもって修正することができるようになるのです。
2つのFIRSTで、テスト駆動開発へ
ここまで挙げた、2つめのFIRSTが合わさると、こうなります。
ユニットテストは
- 最初から
- いつでも
- すぐに
- 何回でも
できるようにしておく
ここまでできるようになってくると、テストを主体に考える開発スタイルに、チェンジしていきます。
そう、それが、テスト駆動開発(TDD:Test Driven Development)と呼ばれるスタイルです。
テスティングフレームワークを使い、テストを自動化し、TDDへシフトチェンジしていきましょう。
まとめ
ユニットテストは、プログラムの品質を高水準で保つ、改修によるデグレードを防ぐ、など、様々なメリットがあります。
なんですが、
プログラマーが、ユニットテストで得られる究極の恩恵、って結局これでしょう。
ユニットテストが充実してくると、こんな感じになってきます。
- やっとバグの原因を突き止め、プログラムを改修。
- おもむろにテストを開始。次々と実行されるテスト群。流れるように表示されるメッセージ。
- そして最後に表示される、No Error。システムオールグリーン、Yeah!
そんな爽快感を味わうために、プログラマーはテストをする。そういっても過言ではないでしょう^^
ユニットテストを楽しみつつ、高品質なソフトウェアを開発しましょう!
コメント