話題の記事

パラメータの正当性検査とユニットテストのカバレッジ

2013.11.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

渡辺です。

最近はユニットテストの導入方法などに関するエントリーが多かったので、今回は実用的な小ネタとして、メソッドにおけるパラメータの正当性検査とユニットテストについて紹介したいと思います。

パラメータの正当性検査

はじめにパラメータの正当性検査について復習しましょう。Javaプログラマであれば読んでないことが許されないEffective Java(第2版P.175、ただし絶版)には次のように記述されています。

ほとんどのメソッドとコンストラクタは、パラメータとして渡される値に関して何らかの制約を持っています。たとえば、インデックス値が負であってはいけないとか、オブジェクト参照がnullであってはいけないというのが普通です。このような制約は明確に文書化すべきであり、メソッド本体の初めに検査することで制約を強制すべきです。これは、エラーが発生したらできるだけ速やかにエラーを検出するようにすべきであるという一般原則の特殊な場合です。そうしないと、エラーが検出される可能性が低くなり、エラーが発見されてもその原因を特定するのがより困難になります。

メソッドの初めに検査しなければ、予期せぬ場所でNullPointerExceptionが送出されたり、まさかnullだとは思わずにコードを修正したらNullPointerExceptionでシステムを止めたりと、酷い目にあうわけです。このため、公開されるメソッド(つまり、publicメソッド)では、次のようにメソッド本体の先頭でパラメータチェックを行うコードを記述するのが良いとされています。

/**
 * 指定した文字列がすべて小文字であるかを判定する。
 * @param str 判定する文字列
 * @return 大文字が含まれない場合にtrue、含まれる場合にfalse
 * @throws NullPointerException 判定する文字列がnullの場合
 */
public boolean isLowerCases(String str) {
    if (str == null) throw new NullPointerException();
    return str.toLowerCase().equals(str);
}

このようなメソッドパラメータにおけるパラメータの正当性検査とユニットテストについて考察していきましょう。

ユニットテスト

それでは、テストコードを書いていく事にします。

正常系をテストする

はじめにこのメソッドの正常系のテストケースを書きましょう。

@Test
public void isLowerCasesはすべて小文字の場合はtrueを返す() {
    // SetUp
    StringChecker sut = new StringChecker();
    boolean expected = true;
    // Exercise
    boolean actual = sut.isLowerCases("hello world");
    // Verify
    assertThat(actual, is(expected));
}

不安ならばもう1テストケースほど追加してください。

純正常系をテストする

次に結果としてfalseが期待される、すなわち大文字が含まれているテストケースを作成するでしょう。

@Test
public void isLowerCasesは大文字が含まれる場合はfalseを返す() {
    // SetUp
    StringChecker sut = new StringChecker();
    boolean expected = false;
    // Exercise
    boolean actual = sut.isLowerCases("Hello world");
    // Verify
    assertThat(actual, is(expected));
}

ここで、「あれ、空文字の時ってどうなるのだろう?」と気付いたのでテストを追加しました。

@Test
public void isLowerCasesは空文字の場合はtrueを返す() {
    // SetUp
    StringChecker sut = new StringChecker();
    boolean expected = true;
    // Exercise
    boolean actual = sut.isLowerCases("");
    // Verify
    assertThat(actual, is(expected));
}

これでテストすれば正常系と純正常系はカバーしました。しかし、異常系、すなわちパラメータがnullの場合は、まだテストしていません。

ここで、EclipseのEclEMMAプラグインを使い、カバレッジを確認してみます。

c1-2

黄色のラインは通過はしているが全てのパスを通っていないラインです。ここでは、if文の中、つまりnullの場合にNPEが送出されるコードが実行されていないというのが可視化されました。このように、カバレッジツールを実行したコードが通ったパスを確認するために利用すると便利です。

さて、isLowerCasesメソッドのカバレッジですが63.6%となっています。この割合が高いのか低いのかはプロジェクトによると思います。もし、不幸にも高いカバレッジを強制されてしまったならば、異常系もテストしなければなりません。

異常系をテストする

今回はテストケースをひとつ追加すれば、異常系のテストもカバーできます。

@Test(expected = NullPointerException.class)
public void isLowerCasesはnullの場合はNPEを送出する() {
    // SetUp
    StringChecker sut = new StringChecker();
    // Exercise
    sut.isLowerCases(null);
}

カバレッジも100%となりました。

c1

どこまでテストすべきか?

さて、ここまで教科書通りにユニットテストのテストケースを積み上げてきました。カバレッジも100%となり、ユニットテストとしては非常に厚く行っている状況です。しかし、ここまで厚くユニットテスト可能な余裕があるとは限りません。むしろ、余裕がない場合が多いと思います。テストケースは効率よく減らすことが大切です。

一方、非常に残念な話ですが、一定のカバレッジを求められている残念な場合もあります。正常系と異常系の2件だけで済ませたいのに、カバレッジが63.6%で目標値に届いていないとしたら・・・。

パラメータの正当性検査を行わない

最もやってはならないのは、パラメータの正当性検査を省略することです。次のようにパラメータの検査を行わないコードであれば、1テストケースであってもカバレッジは100%となります。

/**
 * 指定した文字列がすべて小文字であるかを判定する。
 * @param str 判定する文字列
 * @return 大文字が含まれない場合にtrue、含まれる場合にfalse
 */
public boolean isLowerCases(String str) {
    return str.toLowerCase().equals(str);
}

しかし、予期せぬ状況でNPEが発生し、困るのはプログラマ自身です。これは技術的負債です。

パラメータの正当性検査用のテストケースも追加する

各分岐を網羅するように泣きながらテストケースを追加する場合、注意が必要です。分岐を網羅するようにテストケースを増やしていった場合、重要な純正常系テストケースを忘れがちです。

テストケーストしては、正常系を通す事が一番大切です。分岐を網羅するためにもこれは必要不可欠でしょう。しかし、純正常系のテストケースは、今回のisLowerCasesメソッドのように、正常系のパスの一部に含まれていることがあります。カバレッジを100%にするだけであれば、falseを返すパターンのテストは不要なのです。テストの質とカバレッジとどちらが重要かは言うまでもありません。

パラメータの正当性チェックを外部化する

全テストケースを網羅する余裕もないが、カバレッジも高めなくてはならない、そんな場合はパラメータの正当性検査は外部ユーティリティメソッドに委譲するとシアワセになれます。

例えば、GuavaというGoogleのJava用ライブラリがあります。Guavaには、Preconditionsという事前条件チェック用のユーティリティクラスがあり、これを使ってパラメータの正当性チェックを書き換えると次のようになります。

/**
 * 指定した文字列がすべて小文字であるかを判定する。
 * 
 * @param str 判定する文字列
 * @return 大文字が含まれない場合にtrue、含まれる場合にfalse
 * @throws NullPointerException 判定する文字列がnullの場合
 */
public boolean isLowerCases(String str) {
    Preconditions.checkNotNull(str, "str is null.");
    return str.toLowerCase().equals(str);
}

Preconditions#checkNotNullは名前の通り、事前条件としてnullでないことを検査します。もし、nullの場合は第2引数で指定したメッセージでNPEを送出します。このコードであれば、異常系であるnullの場合のテストケースを追加しなくともカバレッジが100%になります。

c3-2

Guavaを導入できない場合は自作のユーティリティクラスを作ってください。そのユーティリティクラスをテストするだけであればずっと効率的でしょう。

まとめ

まとめます。

  • 速い段階でエラーを検知するためにパラメータの妥当性検査は重要
  • テストケースは正常系、準正常系、異常系と行っていく
  • 妥当性検査をユーティリティメソッドに委譲する方法がある
  • カバレッジを絶対的な指標だと勘違いしない