許可範囲の前・ちょうど・後を数直線とテスト表で示す図。境界値を実行可能にする

設計・テスト・レビュー

JUnitで境界値テストをまとめる方法|ParameterizedTestとCsvSourceの使い方

「1〜100文字を許可する」という仕様をテストするとき、10文字の正常系を一つ書くだけでは、length < 100という間違いを見つけられません。境界の値を表にしても、その表がコードから離れていれば、仕様変更後にテストだけ古くなることもあります。

この記事では、架空の問い合わせ管理で「件名は空白だけを認めず、Unicodeコードポイント数で1〜100」という仕様を採用します。私の結論は、同じ判定を異なる入力で繰り返す境界値だけをパラメータ化することです。null、空白、絵文字の数え方のように失敗の理由が違うものまで、一枚のCSVへ押し込む必要はありません。

先に、境界で何を保証するか決める

次の値を試します。0は拒否、1と100は許可、101は拒否です。2と99も入れると、下限・上限のすぐ内側まで確認できます。仕様の選び方はテストケースの作り方で扱い、ここではその表をJUnitで実行します。

件名の長さ01299100101
許可するか×○○○○×
境界値からパラメータ化テストへ。仕様:許可範囲を決める。境界:前・ちょうど・後。入力と期待値:表の各行へ記入。実行:失敗した条件を読む
境界値からパラメータ化テストへ。CsvSourceで失敗行を特定

たとえば実装を誤って「100未満」に変えると、長さ100の行だけが落ちます。テスト名に入力と期待値を表示しておけば、どの境界を壊したかをログから読めます。

Java 17・JUnit 5.11.4で動かす

以下は独立したMavenプロジェクトの例です。pom.xmlにJUnit Jupiterとテスト実行用のSurefireを入れます。既存のSpring Bootプロジェクトなら、spring-boot-starter-testがJUnitを提供している場合があるので、同じ依存を重複追加せず、プロジェクトのバージョン管理に合わせてください。

<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>title-rule</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.11.4</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build><plugins><plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.5.2</version>
  </plugin></plugins></build>
</project>

判定対象はsrc/main/java/example/TitleRule.javaです。ここでの「文字」はJavaのString.length()が返すUTF-16コード単位ではなく、コードポイントで数えると決めています。

package example;

public final class TitleRule {
    public boolean accepts(String title) {
        if (title == null || title.isBlank()) {
            return false;
        }
        int length = title.codePointCount(0, title.length());
        return length >= 1 && length <= 100;
    }
}

src/test/java/example/TitleRuleTest.javaで、表の一行を一回のテスト実行にします。"a".repeat(length)を使うので、CSVの値は長さと期待結果だけで済みます。

package example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

class TitleRuleTest {
    private final TitleRule rule = new TitleRule();

    @ParameterizedTest(name = "件名{0}文字 → 許可={1}")
    @CsvSource({
        "0, false", "1, true", "2, true",
        "99, true", "100, true", "101, false"
    })
    void lengthBoundary(int length, boolean expected) {
        assertEquals(expected, rule.accepts("a".repeat(length)));
    }

    @Test
    void nullは拒否する() {
        assertFalse(rule.accepts(null));
    }

    @ParameterizedTest(name = "空白だけの件名を拒否: {index}")
    @ValueSource(strings = {" ", " "})
    void whitespaceOnlyIsRejected(String title) {
        assertFalse(rule.accepts(title));
    }

    @Test
    void 絵文字も決めた数え方で判定する() {
        assertTrue(rule.accepts("😀".repeat(100)));
        assertFalse(rule.accepts("😀".repeat(101)));
    }
}

mvn testで、6行の境界値テストと、null・空白・絵文字のテストが実行される想定です。<= 100を< 100へ変えれば、表示名「件名100文字 → 許可=true」の実行が失敗するはずです。この記事の例は原稿作成時点で実行環境による通し検証をしていません。読者の環境で実行した結果を確認してから流用してください。

CsvSourceに向く条件と、分ける条件

@CsvSourceは、入力と期待結果を並べ、各行で同じ一つの判断を確認するときに読みやすい方法です。テスト内でifやswitchを増やし始めたら、ケースごとに確認したい性質が違っていないか見直します。

注意したいのは空値です。JUnitの@CsvSourceでは、引用符なしの空欄はnull、空の引用符で囲んだ値は空文字として扱われます。間違えると「空文字を試したつもりでnullを試していた」状態になります。nullと空白は上の例のように別テストにすると、目的が明確です。文字列にカンマを含める場合にもCSVの引用規則が必要になります。

もう一つ、パラメータ化でテスト数が増えても、仕様を決めた証拠にはなりません。「空白だけは拒否するか」「絵文字は何文字と数えるか」が未決なら、まず仕様を確認します。先にコードの動きに合わせて期待値を書いてしまうと、不具合を固定する危険があります。

どこまで確認できたかを明示する

このテストが保証するのはTitleRuleの判定です。フォームから届く値、HTTPエラー、DB保存時に値が変わらないことは別の確認が要ります。条件の選び方を変えたいならテストケースの作り方へ戻り、外部との境界を考えるならServiceのJUnit・Mockitoテストが次の入口になります。

参照した公式資料

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