仕事で実際に使ってみて、かなり良かった!
なので良かった点を忘れないうちに書いておこう。
良かった点
バグ・考慮漏れに気づく。
主にNullチェック忘れ、if文の条件ミスの発見に役立った。
カバレッジ100%目指すだけで効果大
カバレッジ100%目指すのは賛否両論ありそうだけど、個人的には良かった。
とにかくカバレッジ100を目指せばいいので簡単。
通常のテストでは、テスト仕様書を書くのだけど、それだけだとどうしても考慮漏れが出てくる。
しかし、カバレッジ100目指すという明確な目標があるので、その点では考慮漏れが出ない。何より楽w
余談
ただし、カバレッジ100%を目指しつつ、真面目にメソッドの戻り値も検証するのは大変そう。
目的が異なるテストとして割り切って別々に作るのが良いかも?
- カバレッジ100%:エラー出さずに最後まで走りきれるかという観点
- 戻り値検証:境界値チェック・同地クラス分割・例外テスト
カバレッジ100は全体をざっと見るために作る(戻り値の検証は雑でOK)
戻り値検証でいわゆる通常のテストっぽいケースを書くイメージ。
意外だった点
検証部分は雑でも意外と効果を感じた。
assertEqualsの部分が肝だろうと思っていたけど、大きなメソッドになると戻り値だけに注目せず、途中の処理でエラー出ないか、条件分岐が正しいかをテストできるのも便利だった。
検証結果のエラーで気づくことより、テスト実行でぬるぽエラー出たり、カバレッジ100にならないのおかしいなあで色々気づくことが多かった。
コード例
・テスト対象クラス
package junit; public class SampleBug { private static final String FLAG_ON = "1"; private static final String FLAG_OFF = "0"; private String flag; private String str; //戻り値はあえて意味不明にしています int sampleExecute() { if (str == null && flag.equals(FLAG_ON)) { System.out.println("FLAG_ON"); //コピペの弊害。FLAG_OFFのつもりがFLAG_ONで判定している } else if (str == null && flag.equals(FLAG_ON)) { System.out.println("FLAG_OFF"); } //substring前のnullチェック忘れ str.substring(0, 3); return 1; } }
・テストクラス
package junit; import static org.junit.jupiter.api.Assertions.*; import java.lang.reflect.Field; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SampleBugTest { SampleBug sampleBug; Field fieldStr; Field fieldFlag; @BeforeEach void setup() throws NoSuchFieldException, SecurityException { sampleBug = new SampleBug(); //リフレクションを使ってテスト対象クラスの変数にアクセス //sampleBugクラスのstr変数を取得する fieldStr = sampleBug.getClass().getDeclaredField("str"); //privateの場合でもアクセス可能にする fieldStr.setAccessible(true); fieldFlag = sampleBug.getClass().getDeclaredField("flag"); fieldFlag.setAccessible(true); } @Test void test1() throws IllegalArgumentException, IllegalAccessException { //準備 //テスト対象クラスのflag変数に1をセット fieldFlag.set(sampleBug, "1"); //実行 int actual = sampleBug.sampleExecute(); //検証 assertEquals(1, actual); } @Test void test2() throws IllegalArgumentException, IllegalAccessException { //準備 //テスト対象クラスのflag変数に0をセット fieldFlag.set(sampleBug, "0"); //実行 int actual = sampleBug.sampleExecute(); //検証 assertEquals(1, actual); } }
実際どんな感じでバグに気づけたのか
Nullチェック忘れ
test1を実行したら途中でコケた。
substring前のnullチェックが抜けてたわあ、と気付ける。
ということで直します。(本当は文字列の長さチェックも必要だけど省略)
if(str != null) { str.substring(0, 3); }
if条件間違い
test1とtest2を実行してカバレッジを見てみたらelse ifの方が通っていなかった。
定数FLAG_OFFのつもりが、FLAG_ONになっていると気づけた。
修正後のコード
・テスト対象クラス
package junit; public class SampleBug { private static final String FLAG_ON = "1"; private static final String FLAG_OFF = "0"; private String flag; private String str; //戻り値はあえて意味不明にしています int sampleExecute() { if (str == null && flag.equals(FLAG_ON)) { System.out.println("FLAG_ON"); //FLAG_OFFに修正 } else if (str == null && flag.equals(FLAG_OFF)) { System.out.println("FLAG_OFF"); } //nullチェック追加 if (str != null) { str.substring(0, 3); } return 1; } }
・テストクラス
package junit; import static org.junit.jupiter.api.Assertions.*; import java.lang.reflect.Field; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SampleBugTest { SampleBug sampleBug; Field fieldStr; Field fieldFlag; @BeforeEach void setup() throws NoSuchFieldException, SecurityException { sampleBug = new SampleBug(); //リフレクションを使ってテスト対象クラスの変数にアクセス //sampleBugクラスのstr変数を取得する fieldStr = sampleBug.getClass().getDeclaredField("str"); //privateの場合でもアクセス可能にする fieldStr.setAccessible(true); fieldFlag = sampleBug.getClass().getDeclaredField("flag"); fieldFlag.setAccessible(true); } @Test void test1() throws IllegalArgumentException, IllegalAccessException { //準備 //テスト対象クラスのflag変数に1をセット fieldFlag.set(sampleBug, "1"); //実行 int actual = sampleBug.sampleExecute(); //検証 assertEquals(1, actual); } @Test void test2() throws IllegalArgumentException, IllegalAccessException { //準備 //テスト対象クラスのflag変数に0をセット fieldFlag.set(sampleBug, "0"); //実行 int actual = sampleBug.sampleExecute(); //検証 assertEquals(1, actual); } @Test void test3() throws IllegalArgumentException, IllegalAccessException { //準備 //テスト対象クラスのflag変数にあいうえおをセット fieldStr.set(sampleBug, "あいうえお"); //実行 int actual = sampleBug.sampleExecute(); //検証 assertEquals(1, actual); } }
上記コードのカバレッジ結果
カバレッジの見方&操作
実行方法
カバレッジの見方
右サイドバーに注目。
SapleBug.javaのカバレッジが100%になっている。
■コードの色の意味
- 緑:OK
- 黄色:通ったが全条件を網羅していない
- 赤:通っていない
→詳しくは「Missed Instructions(分岐網羅)とMissed Branches(条件網羅)」のところに書いた。
エビデンスの取得方法
右サイドバーで右クリック→セッションのエクスポート
↓
完了
↓
下記のファイルが作られる。
↓
中身
Missed Instructions(分岐網羅)は100%。
Missed Branches(条件網羅)は90%になっている。
Missed Instructions(分岐網羅)とMissed Branches(条件網羅)
分岐網羅
分岐網羅は文字通り全ての分岐を網羅したら100%になる。C1と表されることもある。
例えば下記のif文がtrueになるケースとfalseになるケースだけ作れば100%。
if (str == null && flag.equals(FLAG_ON)) { System.out.println("FLAG_ON"); //コピペの弊害。FLAG_OFFのつもりがFLAG_ONで判定している }
条件網羅
条件網羅は、個々の条件式の真偽をそれぞれ1回以上含むようにテストすることを条件網羅とをいいます。C2と表されることもあります。
個々の条件式とは、if文を1個と数えるのではなく、if文の中の条件式を1個と数える。
if(条件式1 && 条件式2 && 条件式3)
例えば下記のif文ではこの2ケースでは条件網羅できていない
- (ケース1)条件式1=true && 条件式2=true
- (ケース2)条件式1=false && 条件式2=true
条件式1はtrue/falseの条件が網羅されているが、条件式2はtrueしか条件が無いから。
//条件式1 && 条件式2 if (str == null && flag.equals(FLAG_ON)) { System.out.println("FLAG_ON"); }
なので、もう一つ下記のケースを足す必要がある
- (ケース3)条件式1=true && 条件式2=false
条件網羅するテストコードを書いてみる
・テスト対象
//条件式1 && 条件式2 if (str == null && flag.equals(FLAG_ON)) { System.out.println("FLAG_ON"); }
・テストクラス(分岐網羅はしているが条件網羅はしていない例)
//importや@BeforeEachは省略 @Test void test1() throws IllegalArgumentException, IllegalAccessException { //準備 //テスト対象クラスのflag変数に1をセット fieldStr.set(sampleBug, null);//条件式1=true fieldFlag.set(sampleBug, "1");//条件式2=true //実行 int actual = sampleBug.sampleExecute(); //検証 assertEquals(1, actual); } @Test void test2() throws IllegalArgumentException, IllegalAccessException { //準備 //テスト対象クラスのflag変数に1をセット fieldStr.set(sampleBug, "あいうえお");//条件式1=flse fieldFlag.set(sampleBug, "1");//条件式2=true //実行 int actual = sampleBug.sampleExecute(); //検証 assertEquals(1, actual); }
↓カバレッジ実行
■コードの色の意味
- 緑:OK
- 黄色:通ったが全条件を網羅していない
- 赤:通っていない
↓ テストコードに下記を追加。
- 条件式1=true && 条件式2=false
@Test void test3() throws IllegalArgumentException, IllegalAccessException { //準備 //準備 fieldStr.set(sampleBug, null);//条件式1=true fieldFlag.set(sampleBug, "0");//条件式2=false //実行 int actual = sampleBug.sampleExecute(); //検証 assertEquals(1, actual); }
↓カバレッジ実行
全て緑になった!
MissedBranches(条件網羅)もちゃんと100%になってる!
まとめ
カバレッジ100%を目指すと、Nullチェック漏れやif文の条件間違いに気づける。(分岐網羅100%で良いと思う。)
エクセルでテスト仕様書を書くより楽しい。
カバレッジ100%&戻り値の検証を同時にやろうとすると大変そう。目的は別にあると割り切ったほうが良さそう。
- カバレッジ100%:エラー出さずに最後まで走りきれるかという観点
- 戻り値検証:境界値チェック・同地クラス分割・例外テスト