はじめに
Java開発で頻繁に目にする if (obj != null)
というnullチェックですが、実はテストカバレッジの観点から注意すべき点があることをご存知でしょうか。
テストカバレッジが100%を示しているにもかかわらず、null
の考慮漏れが原因でバグが発生してしまうケースは少なくありません。
この記事では、
- なぜ
if
文によるnullチェックがテスト漏れを見逃しやすいのか - モダンなJavaの
Optional
を用いることで、その問題をどのように解決できるのか
を、具体的なシナリオを通じて解説していきます。
シナリオ:ECサイトの配送先郵便番号を取得する機能
ECサイトで「注文情報(Order
)から、配送先情報(ShippingInfo
)に書かれた郵便番号(zipCode
)を取得する」という機能を例に考えてみましょう。
ここでの要件として、配送先情報がまだ設定されていない(shippingInfo
がnull
の)場合は、事業所のデフォルト郵便番号「100-0001」を返す、という仕様を設けます。
こちらが今回のシナリオで利用するクラスです。
// 配送先情報クラス
class ShippingInfo {
private String zipCode;
public String getZipCode() { /* ... */ }
// ...
}
// 注文情報クラス
class Order {
private ShippingInfo shippingInfo;
public ShippingInfo getShippingInfo() { /* ... */ }
// ...
}
if
文による実装と、そこに潜む課題
この仕様をif
文を使って実装すると、以下のようなコードが考えられます。
public class ZipCodeService {
private static final String DEFAULT_ZIP_CODE = "100-0001";
public String getZipCode(Order order) {
// デフォルト値をあらかじめ変数に設定しておく
String result = DEFAULT_ZIP_CODE;
ShippingInfo info = order.getShippingInfo();
if (info != null) {
// nullでなければ、実際の郵便番号で上書きする
result = info.getZipCode();
}
return result;
}
}
このコードは、一見すると仕様通りに正しく動作するように思えます。
テストカバレッジが示す「落とし穴」
ここで、「配送先が設定されている」正常系のテストコードを書いてみましょう。
@Test
void 配送先情報が存在する場合にその郵便番号を返す() {
var info = new ShippingInfo("150-0002");
var order = new Order(info);
var service = new ZipCodeService();
assertEquals("150-0002", service.getZipCode(order));
}
このテストを実行し、JaCoCoなどのツールでカバレッジを計測すると、getZipCode
メソッドの**行カバレッジは100%**という結果になります。if
文の行も、その内側の行もすべて実行されているからです。
しかし、これは非常に危険な状態です。なぜなら、「配送先情報がnull
の場合に、正しくデフォルト値が返されるか」という重要なロジックがテストで検証されていないにもかかわらず、レポート上は「テストが完了している」ように見えてしまうからです。
if
文には、else
ブロックが記述されていません。null
の場合の処理は、if
ブロックが実行されないことと、変数が初期値のままであることによって、暗黙的に実現されています。この**「暗黙のelse」**こそが、テストの考慮漏れを生み出す温床となります。
Optional
を用いた、より堅牢な解決策
次に、同じ機能をOptional
を用いて実装してみましょう。
import java.util.Optional;
public class ZipCodeServiceWithOptional {
private static final String DEFAULT_ZIP_CODE = "100-0001";
public String getZipCode(Order order) {
return Optional.ofNullable(order.getShippingInfo()) // 1. nullかもしれないオブジェクトをOptionalでラップします
.map(ShippingInfo::getZipCode) // 2. 存在すれば、郵便番号に変換します
.orElse(DEFAULT_ZIP_CODE); // 3. 存在しなければ(nullなら)、こちらのデフォルト値を返します
}
}
この実装が、なぜif
文よりも堅牢と言えるのか、その理由をご説明します。
1. null
の場合の処理が「明示的」になる
Optional
の.orElse()
メソッドは、「もしOptional
が空だった場合に返す値」を指定するものです。これを記述することで、null
の場合の処理をコード上で明確に表現することになります。「暗黙のelse」はなくなり、null
の場合の考慮漏れそのものが起きにくくなります。
2. コードの可読性が向上する
このコードは、手続き的な手順を記述するのではなく、「shippingInfo
があれば郵便番号に変換し、なければデフォルト値を返す」というデータの流れや変換のルールを宣言するようなスタイルで記述できます。これにより、コードの意図が読み手にとって非常に分かりやすくなります。
3. テストすべきケースが明確になる
Optional
のメソッドチェーンは、テストすべき論理的なパスを明確に示してくれます。
-
.map()
が実行されるパス(shippingInfo
が存在するケース) -
.orElse()
が実行されるパス(shippingInfo
がnull
のケース)
開発者はこのコードを見ただけで、両方のケースのテストが必要であると自然に認識できます。また、カバレッジツールもこの分岐を正確に追跡しやすいため、もしnull
の場合のテストが不足していれば、それをレポート上で明確に示してくれます。
まとめ
if (obj != null)
というチェックはシンプルで便利ですが、null
の場合の処理が暗黙的になりやすく、テストカバレッジの計測をすり抜けてしまうリスクを内包しています。
一方でOptional
を用いることで、
null
の場合の処理を明示的に記述させ、考慮漏れを防ぐ- コードの可読性を高め、仕様を直接的に表現する
- テストすべき論理パスを明確にし、テストの網羅性を高める
といったメリットが期待できます。
単なる書き方の違いとしてではなく、null
という存在と向き合い、より安全で保守性の高いコードを設計するための一つの強力な選択肢として、Optional
の活用をご検討いただければと思います。