荷札が最初の郵便番号チェックで止まり、奥の重量チェックまで届かないイラスト

設計・テスト・レビュー

JUnitのassertThrowsが通っても、対象メソッドを実行できていないとき

この記事でわかること

重量オーバーを拒否するJUnitを書いたはずが、荷札を作る処理は実行されていませんでした。入力の準備で同じ型の例外が出ると、assertThrowsが通る理由と、テスト対象の操作だけを囲む修正を確認します。

新人SE
新人SE
2001gを拒否するテストが通ったので、重量チェックはできていると思っていました。実際にはラベルを作れてしまいます。
ポンコツSE
ポンコツSE
JUnitが受け取った例外を見ましょう。ラベルを作る前に、準備した郵便番号で止まっています。

倉庫向けのラベルを作る練習です。郵便番号には半角数字7桁を使い、ラベルを作れる重量は1〜2000g。2001gの荷物を拒否するテストには、条件を満たす郵便番号と、上限を超える重量を用意します。

実行サンプル

荷札を作る処理、準備で通ってしまうテストと修正版の計4件、pom.xml、READMEを収録。JDK 17とMavenで、展開したディレクトリから mvn clean test を実行できます。

郵便番号の準備と、重量の拒否は別の処理

郵便番号はPostalCodeを作るときに確認します。ラベルを作れる重量かどうかはLabelService.createLabelで確認します。呼び出し元は二つの処理を順番に呼び、どちらも不正な引数を受け取るとIllegalArgumentExceptionを投げます。

郵便番号を表すPostalCode.javaの全文です。

package example.labels;

record PostalCode(String value) {
    PostalCode {
        if (value == null || !value.matches("[0-9]{7}")) {
            throw new IllegalArgumentException("郵便番号は半角数字7桁です");
        }
    }
}

重量を確認しラベルを作るLabelService.javaの全文です。

package example.labels;

final class LabelService {
    private static final int MAX_GRAMS = 2_000;

    String createLabel(PostalCode destination, int grams) {
        if (grams <= 0) {
            throw new IllegalArgumentException("重量は1g以上です");
        }
        if (grams > MAX_GRAMS) {
            throw new IllegalArgumentException("重量は2000g以下です");
        }
        return destination.value() + " / " + grams + "g";
    }
}

assertThrowsの中で準備が失敗し、対象メソッドまで届かない

次は、例外の取り違えを観測するMisleadingLabelTest.javaの全文です。assertThrowsに郵便番号を作る行とラベルを作る行を両方渡しています。

package example.labels;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class MisleadingLabelTest {
    @Test
    void showsWhichExceptionMakesTheMisleadingAssertionPass() {
        LabelService service = new LabelService();

        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
            PostalCode destination = new PostalCode("12-345");
            service.createLabel(destination, 2_001);
        });

        assertEquals("郵便番号は半角数字7桁です", thrown.getMessage());
    }
}

new PostalCode("12-345")が先にIllegalArgumentExceptionを投げるため、次のcreateLabelは実行されません。JUnitは渡された処理の途中で期待した型の例外が出れば、そのassertThrowsを成功とします。例では返された例外の文言も読み、出どころが郵便番号の準備だったことを観測しています。重量のチェックを外しても、このテストは成功します。JUnit 5.12.2のassertThrows。

assertThrowsの基本操作や、返り値をassertEqualsで確認する方法から試したい場合は、JUnit入門の3件から始められます。

関連記事JUnit 5入門|Mavenで最初のテストを実行し、失敗を直す JUnitを初めて動かすなら。正常時の戻り値と、例外を投げる入力を同じ送料の例で試せます。 記事を読む

正しい郵便番号を先に作り、createLabelだけを囲む

重量が上限を超えたときのテストでは、まず有効な郵便番号1234567を準備します。その後のassertThrowsに、service.createLabel(destination, 2_001)だけを渡します。正しい郵便番号を作れなければ、テストは準備の行でエラーになり、重量の拒否を確認できた扱いにはなりません。

修正後のLabelServiceTest.javaの全文です。2001gの拒否に加え、上限ちょうどの2000gでラベルを作れること、0gの拒否もそれぞれ確認します。

package example.labels;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class LabelServiceTest {
    private final LabelService service = new LabelService();

    @Test
    void rejectsParcelAboveTheMaximum() {
        PostalCode destination = new PostalCode("1234567");

        assertThrows(IllegalArgumentException.class,
                () -> service.createLabel(destination, 2_001));
    }

    @Test
    void createsLabelAtTheMaximum() {
        PostalCode destination = new PostalCode("1234567");

        assertEquals("1234567 / 2000g", service.createLabel(destination, 2_000));
    }

    @Test
    void rejectsZeroWeight() {
        PostalCode destination = new PostalCode("1234567");

        assertThrows(IllegalArgumentException.class,
                () -> service.createLabel(destination, 0));
    }
}

ラムダを1行にしても、createLabel(new PostalCode("12-345"), 2_001)のように引数の中で準備すると、先に郵便番号の処理が走ります。行数よりも、そのラムダを実行したときに先に何が評価されるかを確認してください。assertThrowsExactlyで例外の型を厳密にしても、この例の二つの処理は同じ型を投げるため、取り違えは残ります。

上限チェックを外し、どのテストが失敗するか確かめる

ZIPを展開し、pom.xmlがある場所でmvn clean testを実行します。通常版は4件成功します。次にLabelService.javaのgrams > MAX_GRAMSを確認するifブロックだけを外して、もう一度実行します。

実行条件準備の例外を拾うテスト2001gの拒否を確認するテスト
通常版成功。郵便番号で例外成功。ラベル作成で例外
重量の上限チェックを外す成功。郵便番号で例外失敗。ラベル作成で例外が出ない
上限チェックを戻し、修正版の郵便番号だけ不正にする成功。郵便番号で例外エラー。assertThrowsの前の準備で例外

上限チェックを外した実行では、2001gのテストに「期待した例外が投げられなかった」という失敗が出ます。これで、修正版のテストが重量の判定を実際に見ていることを確認できます。修正後にコードを戻し、4件が成功することも確かめます。

pom.xmlの全文を見る
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>junit-assertthrows-exception-source</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.12.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.14.1</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.3</version>
      </plugin>
    </plugins>
  </build>
</project>

レビューでは、例外を期待する操作を一つに絞る

レビューでは「2001gを拒否するケースなので、郵便番号は有効な値を先に用意してください。assertThrowsにラベルを作る呼び出しだけを渡すと、重量チェックを外したときにテストが落ちます」と伝えます。郵便番号の拒否も保証したいなら、別のテストでPostalCodeを作る操作を囲みます。

  • テスト名と、例外を期待するメソッドが対応している。
  • 引数の生成や事前準備は先に済ませている。
  • ラムダの引数内で、先に例外を投げる処理がない。
  • 対象のチェックを外すと、テストが失敗することを確認した。

次は、どの入力を試すかを仕様から決める

例外の出どころを分けられたら、正常と異常を切り替える値をどう選ぶかが次の課題です。『ソフトウェアテストの教科書[増補改訂 第2版]』は、境界値や同じ扱いになる入力のまとめ方を学び直したいときの1冊です。今回なら2000gと2001gを並べ、どちら側が仕様上の正解かを自分の言葉で説明します。

書籍「ソフトウェアテストの教科書[増補改訂 第2版]」の表紙
書影:SBクリエイティブ
PR テストを学ぶ若手向け

ソフトウェアテストの教科書[増補改訂 第2版]

布施昌弘・江添智之・永井努・三堀雅也 著

仕様から、試す入力と期待する結果を選べるようになる。

同値分割・境界値や条件の組合せなど、テストケースを選ぶ考え方を基礎から学ぶ1冊。何を試せばよいかわからない段階で選びます。

  • 正常な値を1件試した後、追加するケースに迷う
  • 入力と期待値を選んだ理由を説明したい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

関連記事テストケースの作り方|仕様・境界値・期待結果をJUnitと対応させる実例 例外を確認する操作が決まったら、境界や状態からどんな入力を残すか考えます。 記事を読む 関連記事Javaのテスト入門におすすめの本|テストケースを選べる最初の1冊 JUnitの書き方に加え、どの入力や期待値を選ぶかを整理したい人のための選書ガイドです。 記事を読む

まとめ

assertThrowsは、渡した処理で例外が出たことを確かめます。どの操作で例外が出るべきかを決め、有効な引数を先に準備して、その操作だけを囲みます。確認したいチェックを外してテストが落ちるか試すと、準備の例外で通っていたケースを見つけられます。

-設計・テスト・レビュー
-, ,