プログラム開発の中では、あまり派手とはいえない、テスト作業。
欠かすことはできない。けれど、コーディングと比べれば、地味で面白味を感じない…
でも、そんなテストが、作業の中心となる技法があります。それが、テスト駆動開発です。
テスト駆動開発とは何か?
テスト駆動開発(TDD)は、プログラミングにおける、テストのやり方のひとつです。
Test-Driven Development (TDD)
まずテストを書き、テストの動作を確認しながら開発する手法
一般的なプログラム開発の順序
TDDではない、一般的なプログラム開発の場合。まずプログラムのコードを書き、そのあとに動作させるためのテストコードを書いて、テストします。
- プログラムのコードを書く
- プログラムを動作させるためのテストコードを書く
- テストする
作業工程通りの、ノーマルなやり方です。
TDDでのプログラム開発の順序
対して、TDDの場合、開発手順はこのようになります。
- まずテストコードを書く
- プログラムコードを書きながらテストする
- テストが通るまでプログラムコードを書く
まずテストコードを書きます。最初は、動くプログラムがないので、テストは必ず失敗します。
次にコードを書きます。コードを書く、テストする、を何度も繰り返します。テストが全部通ったら、完成です。
プログラムを書く前に、まずテストプログラムを書く。これがTDDの特徴です。
存在しないプログラムのテストが書けるのか?
まだ存在しないプログラムコードに対して、テストコードを書く。
そんなこと、できるのでしょうか?レーシングチームのテストドライバーに、「まだマシンはないけどテストドライブしよう」とか無茶なこと言ってるような。
もちろん、どんなテストにも適用できるわけではないです。TDDを行う上では前提があります。
TDD前提条件(1):ユニットテストが前提
テストと一口にいっても、実際のリリースまでには複数の段階があります。
呼び方はいろいろありますが、おおよそ以下の4段階があります。
- 単体テスト(Unit Testing) 個々のクラス、メソッド、モジュールが正常に動作するかを確認
- 結合テスト(Integration Testing) モジュールを組み合わせたときに正常に動作するかを確認
- システムテスト(System Testing) システムの要件や仕様を満たしていることを確認
- 受け入れテスト(Acceptance Testing) リリースの準備ができているかを確認
このうち、TDDは、主に単体テスト(Unit Testing)の段階で適用します。最初の段階の、個々のクラスやメソッドに対するテストです。
結合テスト以降でTDDを適用するのは相当に難しいです。なにもプログラム作ってないのに受け入れテストコード書く、なんてちょっと不可能すぎます。
TDD前提条件(2):ブラックボックステストが前提
単体テストにも、やり方が実は2種類あります。
- ブラックボックステスト クラスやメソッドの中身を見ずに行うテスト、入力に対し適切な出力がされているかを確認するテスト
- ホワイトボックステスト クラスやメソッドの中身を見て行うテスト、ロジックの正当性や分岐網羅性を確認するテスト
この2つは、どちらかを選択するというより、たいてい両方行います。
TDDは、ブラックボックステストで適用します。それはそうですね。クラスやメソッドの中身がまだないのですから。
TDDは「契約による設計」を確認するテスト
「入力に対し適切な出力がされているかを確認する」、これはまさに、契約による設計の内容です。
TDDは、契約が守られているかを確認するためのテスト、ともいえます。
TDDのプログラム実装方法
TDDの概念が分かったところで、実際にTDDのプログラミングを見ていきましょう。
ここでは例として、「Javaで実装した素数判定メソッド」をテストする例を見てみます。
テスト対象プログラムのインターフェースだけつくろう
テストを先につくる、といっても、さすがにテスト対象プログラムの定義くらいは必要です。
まず、テスト対象プログラムの、インターフェースだけ実装しておきましょう。単にtrueを返却するだけの中身ないメソッドです。
public class PrimeNumberChecker {
// 素数を判定するメソッド
public boolean isPrime(int number) {
// とりあえずtrueを返す
return true;
}
}
テスティングフレームワークを使ってテストを実装しよう
テストプログラムを書くのですが、一から書くのは効率が悪い。
今どきは大抵の言語に、テスティングフレームワークがあります。テストをかんたんに書くためのフレームワークです。
ここでは、Javaの有名なテスティングフレームワーク、JUnitを使ってみましょう。
1~4のそれぞれの数を入力して、素数の判定が正しいか確認するテストです。
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 primeNumberChecker = new PrimeNumberChecker();
Assertions.assertFalse(primeNumberChecker.isPrime(1));
}
// 2は素数
@Test
public void testTwoIsPrime() {
PrimeNumberChecker primeNumberChecker = new PrimeNumberChecker();
Assertions.assertTrue(primeNumberChecker.isPrime(2));
}
// 3は素数
@Test
public void testThreeIsPrime() {
PrimeNumberChecker primeNumberChecker = new PrimeNumberChecker();
Assertions.assertTrue(primeNumberChecker.isPrime(3));
}
// 4は素数でない
@Test
public void testFourIsNotPrime() {
PrimeNumberChecker primeNumberChecker = new PrimeNumberChecker();
Assertions.assertFalse(primeNumberChecker.isPrime(4));
}
}
テストを実行しよう
ここで、さっそくテストを実行してみましょう。
テスティングフレームワークは、テストメソッドを書いて実行すれば、自動的にすべてのテストを実行してくれます。
> Task :test FAILED
PrimeNumberCheckerTest > testOneIsNotPrime() FAILED
org.opentest4j.AssertionFailedError at PrimeNumberCheckerTest.java:13
PrimeNumberCheckerTest > testFourIsNotPrime() FAILED
org.opentest4j.AssertionFailedError at PrimeNumberCheckerTest.java:34
PrimeNumberCheckerTest > testThreeIsPrime() PASSED
PrimeNumberCheckerTest > testTwoIsPrime() PASSED
テストが失敗(FAILED)になっていますね。まだ本体を作ってないので、当然ですね。
プログラム本体を作ろう
プログラムの本体、素数判定メソッドを実装しましょう、判定方法はいろいろありますが、ここではごく単純な方法でやります。
public class PrimeNumberChecker {
// 素数を判定するメソッド
public boolean isPrime(int number) {
if (number <= 1) {
return true;
}
// 2からnumber-1までの数で割り切れるかどうかを調べる
for (int i = 2; i < number; i++) {
if (number % i == 0) {
return false;
}
}
return true;
}
}
作ったプログラムをテストしよう
実装したら、すぐさまテストを実行しましょう。
> Task :test FAILED
PrimeNumberCheckerTest > testOneIsNotPrime() FAILED
org.opentest4j.AssertionFailedError at PrimeNumberCheckerTest.java:13
PrimeNumberCheckerTest > testFourIsNotPrime() PASSED
PrimeNumberCheckerTest > testThreeIsPrime() PASSED
PrimeNumberCheckerTest > testTwoIsPrime() PASSED
おや…まだテストが失敗します。1を入力したときの判定が間違っているようです。
原因はどこでしょう…
public class PrimeNumberChecker {
// 素数を判定するメソッド
public boolean isPrime(int number) {
if (number <= 1) {
return true;
}
// 2からnumber-1までの数で割り切れるかどうかを調べる
for (int i = 2; i < number; i++) {
if (number % i == 0) {
return false;
}
}
return true;
}
}
ここが間違いですね。1の場合は素数でないので、return false が正解。
直してまたテストしましょう。
> Task :test
PrimeNumberCheckerTest > testOneIsNotPrime() PASSED
PrimeNumberCheckerTest > testFourIsNotPrime() PASSED
PrimeNumberCheckerTest > testThreeIsPrime() PASSED
PrimeNumberCheckerTest > testTwoIsPrime() PASSED
テストがすべて通りましたね。
いろいろな言語のテスティングフレームワーク
ここではJavaの例を取り上げましたが、テスティングフレームワークは、たいていのプログラム言語に存在します。
言語 | テスティングフレームワーク |
---|---|
C++ | CppUTest、Google Test、Boost.Testなど |
Java | JUnit、TestNGなど |
C# | NUnit、xUnit.net |
Python | unittest、pytestなど unittestはPython標準 |
PHP | PHPUnitなど |
Ruby | Minitest、RSpec、test-unitなど MinitestはRuby標準 |
JavaScript | Jasmine、Mocha、Jestなど |
Go | Go標準のtestingパッケージがある |
Rust | Rust標準のテストフレームワークがある |
どの言語のテスティングフレームワークも、
- テストメソッドの自動実行
- テスト結果の自動判定
- テスト結果のレポート出力
といった、豊富な機能を備えています。
TDDのメリット3つ
TDDのメリットは、大きくつぎの3つ。
- バグを早期発見できる
- プログラムのインタフェースが明解になる
- リグレッションテストが容易になる
メリット(1)バグを早期発見できる
バグがあったら、すぐに直して、テストする。このサイクルが簡単に回せます。
知らぬ間に入り込んだバグを早期に発見できる、といった効果がありますね。
メリット(2)プログラムのインターフェースが明確になる
テストを作るということは、テスト対象ブログラムを使うプログラムを実装する、ということになります。
これにより、使う側から見た「インタフェースの分かりやすさ」を早期に確認することができます。
これは、依存性逆転原則が保たれているか、の確認にも有効です。
メリット(3)リグレッションテストが容易になる
プログラム開発が進むと、ある修正が、ほかの思わぬ箇所に影響を及ぼすことかあります。
テストを自動化しておくことで、修正時の再試験(リグレッションテスト)が容易になります。
修正したらすぐにテスト、また修正したら即テスト。これで思わぬバグが発見できます。
TDDの注意事項3つ
使えば便利なTDD。しかし、やりさえすればあらゆる問題を解決するという魔法の手法ではありません。
注意すべきことは、つぎの3つ。
- テストは実装中も見直すこと
- テストコードにこだわりすぎないこと
- 参照透過性をもつテストにすること
注意(1)テストは実装中も見直すこと
TDDは、前述のとおり、ブラックボックステストが前提です。
しかしながら、ホワイトボックステストも重要。プログラム実装中や実装後に、ホワイトボックステストの追加も検討しましょう。
注意(2)テストコードにこだわりすぎないこと
テストプログラムの作り方は、プログラマーの裁量に委ねられることが多いでしょう。
そうすると、妙にマニアックなプログラムを書いて、いたずらに時間を浪費してしまうときがあります。
テストプログラムを作るときだって、KISS原則 や YAGNI原則 は意識しましょう。
注意(3)参照透過性をもつテストにすること
参照透過性:同じ入力に対して同じ出力が返る特性、のこと
そんなの当たり前?いやそうでもないのです。
ファイル、データベース、ネットワークなど、外部の状態によって結果が変わる処理があります。
「商品購入処理」で、「商品データベースに在庫あれば1個減、なければエラー」など…こういった処理はTDDが非常に難しくなります。
テスト対象メソッドが参照透過性を持たない場合、TDDの適用が難しくなります。結果を定義できないですからね。
TDDが結合テスト以降での適用が難しいのは、この点です。
こういった処理は、依存性注入などの工夫で回避する、という手もあります。
まとめ
TDDは。プログラムの開発効率や品質の向上に大きな効果をもたらします。
でもヘンにこだわりすぎるとかえって逆効果。現実目線でバランスのよい使い方をしましょう。
コメント