先日、Microsoft の開発者向けイベント de:code2017 に初参加しました。
最新の技術動向から、レガシー系開発現場をどうマネジメントし変えていくか、と幅広いテーマの話を聞くことが出来、とても面白かったです。
中でも DO03 50 分でわかるテスト駆動開発 というセッションのライブコーディングの題材であった、FizzBuzz 問題のテスト駆動開発 がとても印象的でした。
そこで、そもそもテスト (単体テスト) とは? から振返りつつ、自分なりに実践してみました。
実践内容を順追っていくと長くなりそうなので、準備編と実践編の 2 つに分けて投稿します。今回は、準備編です。
単体テストとは
一言で言うと
単体テストとは、クラスやメソッド等の小さな単位でプログラムが仕様書通りに動作するかを確認するテストです。
単体テストの手法
主に、以下の 3 つに分けられます。
-
機能確認テスト
プログラムが仕様書通りに動作することを確認する -
制御フローテスト
命令文、条件分岐が全て実行されるかを確認する -
データフローテスト
データ (特に他と共通で使用されるデータ) が定義・使用・解放されているかを確認する
どうして単体テストが必要か
単体テストは役に立たない、という考えもある1ようですが、自身は以下の点で必要であると感じています。
-
小さな部品からボトムアップで品質を積み上げる。
-
リファクタリングや仕様変更後の動作確認を容易にし、システムのメンテナンス性を向上させる。
テスト駆動型開発とは
一言で言うと
仕様書の各機能について、以下の 3 つのサイクルを繰り返す開発手法のことです。
- テストコードを書く
- テストが通るプログラムを実装する
- リファクタリングする
xUnit のフレームワークではテスト失敗 / 成功時のバー表示の色からこのサイクルのことを RED - GREEN - REFACTOR とも言います。
どうしてテスト駆動開発を選択するか
テスト駆動開発を実際に選択するかはプロジェクトの状況によりますが、以下のメリットはどんなプロジェクトでも当てはまるかと思います。
- 動く綺麗なコードを実現出来るため (Clean code that works)。
- 実装を終えてから、テストで出たバグの影響範囲全てを修正していくという手戻りを少なくできるため。
- 保守性の高い設計を実現出来るため (テストしやすい = 変更に強く保守性が高い ↔ テストしにくい = 設計改善の余地あり)
では早速 FizzBuzz 問題のテスト駆動開発 の準備に入っていきましょう。
動作環境の準備
環境設定
Type | Material | Version |
---|---|---|
Language | Java (JDK) | 1.8.0_111 |
IDE | Spring Tool Suite | 3.8.2 |
Build | Maven | 3.3.9 |
Test | JUnit | 4.12 |
プロジェクトの作成
以下の手順でプロジェクトを作成していきます。
- STS (Spring Tool Suite) を立ち上げる。
- Package Explorer で右クリック -> New -> Maven Project をクリック
- "Create a simple project (skip archetype selection)" にチェックを入れ、Next をクリック
- Group Id と Artifact Id に任意のパッケージ名を入れて Finish をクリック
作成されたプロジェクトの直下に pom.xml が作成されていることを確認します。
pom.xml の設定
次に、JUnit を使用するため、pom.xml に以下の設定を追加します。
<!-- 関係部分だけ抜粋 -->
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
補足
JRE のライブラリのバージョンが 1.8 となっていない場合は、pom.xml に maven-compiler-plugin の設定を追加して下さい。
<!-- 関係部分だけ抜粋 -->
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
FizzBuzz 問題の要件定義
環境が整ったところで、次は実装する要件を定義します。
FizzBuzz 問題
1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
元は英語圏の言葉遊びだそうですね。
FizzBuzz 問題の機能要件を分割する
上記の問題から、キーワードを抜き出して要件を分割し、実装部分を定義します。
キーワードを抜き出す
- 1 から 100 までの数
- プリントする
- 3の倍数のときは「Fizz」
- 5の倍数のときは「Buzz」
- 3と5両方の倍数の場合には「FizzBuzz」
実装する部分を定義する
キーワードから、テスト対象クラスに実装する部分を定義します。
ここで、プリントする機能は FizzBuzz クラスの呼び出し側に任せることにして、FizzBuzz クラスには呼び出し側がプリントしたい文字列を返す、という機能を持たせます。
- 数を引数に取る
- 文字列を返す
プリントする- 引数の値
- ただし、3の倍数のときは「Fizz」
- ただし、5の倍数のときは「Buzz」
- ただし、3と5両方の倍数の場合には「FizzBuzz」
- 引数に取るのは 1 から 100 まで
実装する順番を決定する
クラスの入口と出口である " 数を引数に取る " と " 文字列を返す " を除くとしても、上記のうちどの機能から手をつけていくか? という疑問が残ると思います。
時と場合にもよると思うのですが、私自身は以下の機能から着手する考えで順番を決めました。2
-
例外発生のないもの
" 引数に取るのは 1 から 100 まで " = " 引数は 0 以下 または 101 以上であると例外発生 " となるので、最後にする -
分岐条件のないもの
" ただし " が着かないものを優先する -
分岐条件がより単純であるもの
" 3と5両方の倍数の場合 " は条件が 2 つなので、後にする - 最終要件をより早く満たすもの (注釈参照)
ということで、数を引数に取って文字列を返す FizzBuzz クラスを、以下の順番でテスト駆動開発していきます。
- 引数に取った値を文字列で返す
- ただし、3の倍数のときは「Fizz」 を返す
- ただし、5の倍数のときは「Buzz」 を返す
- ただし、3と5両方の倍数の場合には「FizzBuzz」を返す
- 引数が 1 から 100 までの数でない場合、エラー
JUnit の動作確認
まずは、テストコードの作成…といきたいところですが、その前に JUnit が正しくインストールされているかを確認する必要があります。
具体的には、テストクラス FizzBuzzTest.java を準備し、空のテストプログラムを書き、テストを実行します。
FizzBuzzTest.java の作成
まず、テストクラスを以下の手順で準備します。
- "src/test/java" にカーソルを当て、右クリック -> New -> Package をクリック
- Name 欄に任意のパッケージ名を入れ、Finish をクリック
- 作成したパッケージにカーソルを当て、右クリック -> Class をクリック
- Name 欄に FizzBuzzTest と入力し、Finish をクリック (その他の設定はデフォルトのまま)
FizzBuzzTest.java 作成後のプロジェクト構成は以下のようになります。
空のテストプログラム作成
次に、作成した FizzBuzzTest.java に何も処理を行わない空のテストメソッドを実装します。
import org.junit.Test;
public class FizzBuzzTest {
@Test
public void JUnit動作テスト() {
}
}
テスト実行
作成した FizzBuzzTest クラスの JUnit動作テストメソッドにカーソルを当て、右クリック -> Run As -> JUnit Test をクリックしてテストを実行します。
すると、以下の図のようにテストが成功したことを示す緑のバーが表示されます。
これより、デフォルト通りに JUnit が動作しており JUnit を使用する準備が整っていることが分かります。
逆に、テストに失敗してしまうと、例えば Maven によるインストールが正しく行えていなかった等のテスト環境の不備を発見できます。
JUnit が問題なく動作することを確認できたところで、準備編 は終わりです。
ここで、Conclusion comes first ということで最終的なプロジェクト構成とプログラムの内容を載せておきます。3
プロジェクト構成
テストプログラム
package com.example;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
@RunWith(Enclosed.class)
public class FizzBuzzTest {
public static class 引数が3と5の倍数でない {
FizzBuzz fizzbuzz = new FizzBuzz();
@Test
public void 引数に1を与えたら1を返す() {
assertEquals("1", fizzbuzz.response(1));
}
}
public static class 引数が3のみの倍数 {
FizzBuzz fizzbuzz = new FizzBuzz();
@Test
public void 引数3を与えたらFizzを返す() {
assertEquals("Fizz", fizzbuzz.response(3));
}
}
public static class 引数が5のみの倍数 {
FizzBuzz fizzbuzz = new FizzBuzz();
@Test
public void 引数5を与えたらBuzzを返す() {
assertEquals("Buzz", fizzbuzz.response(5));
}
}
public static class 引数が3と5の倍数 {
FizzBuzz fizzbuzz = new FizzBuzz();
@Test
public void 引数15を与えたらFizzBuzzを返す() {
assertEquals("FizzBuzz", fizzbuzz.response(15));
}
}
public static class 引数が有効境界値 {
FizzBuzz fizzbuzz = new FizzBuzz();
@Test
public void 引数1を与えたら1を返す() {
assertEquals("1", fizzbuzz.response(1));
}
@Test
public void 引数100を与えたらBuzzを返す() {
assertEquals("Buzz", fizzbuzz.response(100));
}
}
public static class 引数が無効境界値 {
FizzBuzz fizzbuzz = new FizzBuzz();
@Test(expected = IndexOutOfBoundsException.class)
public void 引数0を与えたらエラーとなる() {
fizzbuzz.response(0);
}
@Test(expected = IndexOutOfBoundsException.class)
public void 引数101を与えたらエラーとなる() {
fizzbuzz.response(101);
}
}
}
テスト対象プログラム
public class FizzBuzz {
public String response(int num) {
StringBuilder result = new StringBuilder();
if(num < 1 || num > 100) {
throw new IndexOutOfBoundsException();
}
if(num % 3 == 0) {
result.append("Fizz");
}
if(num % 5 == 0) {
result.append("Buzz");
}
if(result.length() == 0) {
result.append(String.valueOf(num));
}
return result.toString();
}
}
次回は、以下のテストクラス (FizzBuzzTest.java) とテスト対象クラス (FizzBuzz.java) の実装をどう進めたかを 実践編 として投稿したいと思います。
参考
-
Amazon.co.jp: 【この1冊でよくわかる】ソフトウェアテストの教科書―品質を決定づけるテスト工程の基本と実践 eBook: 石原 一宏, 田中 英和, 田中 真史: Kindleストア
-
テスト駆動開発とは何か、それを気に入っているのは何故か、あなたも使うべきなのは何故か | 開発手法・プロジェクト管理 | POSTD
-
私は要約しか読んでいないですが、元ネタは Why-Most-Unit-Testing-is-Waste.pdf
に書かれています。 ↩ -
例えば、ユーザ特性に応じた複数の割引オプションがある映画の予約システムにおいて、最終要件が " 割引率が高くなるオプションを適用する " ならば、割引率が高くなるユーザ特性条件からテストしていきます。 ↩
-
自身で実践した結果であることと、de:code では時間制約上省略された部分もあったので、参考にした de:code でのライブコーディングと一部異なる部分があります。 ↩