0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

if (obj != null)に潜むテストカバレッジの罠と、Optionalによる堅牢なコード設計

Posted at

はじめに

Java開発で頻繁に目にする if (obj != null) というnullチェックですが、実はテストカバレッジの観点から注意すべき点があることをご存知でしょうか。

テストカバレッジが100%を示しているにもかかわらず、nullの考慮漏れが原因でバグが発生してしまうケースは少なくありません。

この記事では、

  • なぜ if文によるnullチェックがテスト漏れを見逃しやすいのか
  • モダンなJavaの Optional を用いることで、その問題をどのように解決できるのか

を、具体的なシナリオを通じて解説していきます。

シナリオ:ECサイトの配送先郵便番号を取得する機能

ECサイトで「注文情報(Order)から、配送先情報(ShippingInfo)に書かれた郵便番号(zipCode)を取得する」という機能を例に考えてみましょう。

ここでの要件として、配送先情報がまだ設定されていない(shippingInfonullの)場合は、事業所のデフォルト郵便番号「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()が実行されるパス(shippingInfonullのケース)

開発者はこのコードを見ただけで、両方のケースのテストが必要であると自然に認識できます。また、カバレッジツールもこの分岐を正確に追跡しやすいため、もしnullの場合のテストが不足していれば、それをレポート上で明確に示してくれます。

まとめ

if (obj != null)というチェックはシンプルで便利ですが、nullの場合の処理が暗黙的になりやすく、テストカバレッジの計測をすり抜けてしまうリスクを内包しています。

一方でOptionalを用いることで、

  • nullの場合の処理を明示的に記述させ、考慮漏れを防ぐ
  • コードの可読性を高め、仕様を直接的に表現する
  • テストすべき論理パスを明確にし、テストの網羅性を高める

といったメリットが期待できます。

単なる書き方の違いとしてではなく、nullという存在と向き合い、より安全で保守性の高いコードを設計するための一つの強力な選択肢として、Optionalの活用をご検討いただければと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?