JUnit 最初のテスト。置く・動かす・失敗を読む。紙の本体とテストから実行結果へ進み、期待0円と実際500円の違いを見るイラスト。

設計・テスト・レビュー

JUnit 5入門|Mavenで最初のテストを実行し、失敗を直す

テスト対象のShippingFee.javaと、テストコードのShippingFeeTest.javaをセットで動かし、JUnitの使い方を覚えます。まず送料計算の3件を成功させ、本体の条件を1か所変えて失敗させます。最後に、その失敗表示から直す場所を見つけます。

この記事でわかること

  • 本体・テスト・Mavenの設定を、どのファイルに置くか。
  • assertEqualsで戻り値を、assertThrowsで例外を確認する書き方。
  • expected: <0> but was: <500>から、仕様と実装のどこを見直すか。

「送料の変更にテストも付けて」と言われたとき、画面で1回確かめるところから、繰り返し実行できるテストへ進むための記事です。題材は架空の送料計算。Javaのclass、new、メソッド呼び出しを読める人を対象に、手を動かす順に進めます。

目標はJUnitのAPIを全部覚えることではありません。何を入力し、何を正解とし、失敗したらどこを見るかを、この3件について説明できることです。

サンプルをダウンロードし、本体とテストの2ファイルを開く

冒頭のZIPを展開すると、junit-first-testフォルダができます。まずsrc/main/java/ShippingFee.javaとsrc/test/java/ShippingFeeTest.javaを、IDEやエディターで開いてください。以下では、この2ファイルに書かれている処理を順に説明します。

この例はJava 17、Maven 3.9系、JUnit Jupiter 5.12.2で動かします。java -versionとmvn -versionを実行し、Mavenが使うJavaも17になっていることを確認してください。mvnが見つからない場合は、Mavenを実行できる環境を先に用意します。業務プロジェクトへ混ぜず、展開した練習用フォルダを使います。

junit-first-test/
├── pom.xml
└── src/
    ├── main/java/ShippingFee.java
    └── test/java/ShippingFeeTest.java

src/main/javaは実際に使う処理、src/test/javaはその処理を確認するコードの置き場所です。どちらもJavaですが、テストのためだけに必要なJUnitを本体へ持ち込まないよう分けます。この短い例ではpackage宣言を省いています。業務のコードでpackageを使う場合は、宣言とフォルダ階層も合わせてください。

テスト対象:ShippingFee.javaは、小計から送料を返す

今回の仕様は、小計10,000円以上なら送料0円、それ未満なら500円です。小計は整数の円で、0円から1,000,000円までを受け付けます。その範囲外はIllegalArgumentExceptionにします。0円を受け付けるのは、この練習の約束です。

開くファイルはsrc/main/java/ShippingFee.javaです。次が全文です。JUnitのコードはここにはありません。このクラスが送料を決め、次の章のテストがその結果を確かめます。

/** 金額の単位は円。小計は0円から1,000,000円まで。 */
public final class ShippingFee {
    public int calculate(int subtotal) {
        if (subtotal < 0 || subtotal > 1_000_000) {
            throw new IllegalArgumentException("小計は0円以上100万円以下です");
        }
        return subtotal >= 10_000 ? 0 : 500;
    }
}

本体のif (subtotal < 0 || subtotal > 1_000_000)は、小計が許可した範囲から外れていないかを調べます。-1ならここで例外を投げ、下のreturnへは進みません。範囲内なら、subtotal >= 10_000 ? 0 : 500で、条件を満たすと0、満たさないと500を返します。次のテストのうち、9,999円と10,000円の2件はこの戻り値、-1の1件は最初の範囲チェックを確かめます。

calculateが返すのは送料です。商品代金を含めた合計ではありません。したがって、9,999円を渡したときの正解は10,499円ではなく500円です。テストを作る前に戻り値の意味を決めると、別の値を正解として書く取り違えを防げます。

テストコード:ShippingFeeTest.javaから本体を呼び、結果を比べる

次にsrc/test/java/ShippingFeeTest.javaを開きます。次が全文です。本体の名前にTestを付けた、このファイルをJUnitが実行します。このMaven設定で実行対象になる名前なので、練習ではShippingFeeCheckなどへ変更しないでください。

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

class ShippingFeeTest {
    private final ShippingFee shippingFee = new ShippingFee();

    @Test
    void subtotal9999Costs500() {
        assertEquals(500, shippingFee.calculate(9_999));
    }

    @Test
    void subtotal10000IsFree() {
        assertEquals(0, shippingFee.calculate(10_000));
    }

    @Test
    void negativeSubtotalIsRejected() {
        assertThrows(IllegalArgumentException.class,
                () -> shippingFee.calculate(-1));
    }
}

最初に読むのは、private final ShippingFee shippingFee = new ShippingFee();です。ここで、先ほどのテスト対象を1つ作り、shippingFeeという名前で使えるようにしています。テスト側へ送料計算をもう一度書くのではなく、shippingFee.calculate(...)で本体の処理を呼び出します。

コード中の9_999や10_000は、桁を読みやすく区切った整数です。値は9999、10000と同じです。

最初のテストは、shippingFee.calculate(9_999)を実行し、戻り値が500であることを確認します。assertEquals(期待値, 実際の値)の順です。左の500は先ほどの仕様から決めた送料、右は本体を動かした結果です。メソッド名subtotal9999Costs500は、「小計9,999円なら送料500円」という条件と結果を表しています。

2件目は、10,000円を渡して0円になることを確認します。9,999円と10,000円は1円しか違いませんが、「10,000円以上」という条件の両側にいます。今回はJUnitを実行する流れを理解するため、この2つへ絞ります。この3件で入力範囲を網羅した、という意味ではありません。

@Testを付けたメソッドが実行対象です。importはorg.junit.jupiter.api.Testを使います。古いJUnitのorg.junit.Testと混ぜないようにしてください。assertEqualsの手前にAssertions.がないのは、冒頭のimport staticで呼び出し名を省略しているためです。

3件目のassertThrowsでは、戻り値ではなく例外を確認します。IllegalArgumentException.classが期待する例外、() -> shippingFee.calculate(-1)が実行してもらう処理です。() ->の部分は、すぐに呼び出すのではなく「この処理を実行して例外を確かめて」と渡す書き方です。

例外が起きない場合や、指定した例外とそのサブクラス以外の例外が起きた場合、この確認は失敗します。assertThrowsは、指定した例外のサブクラスも受け付ける確認です。今回決めたのは小計の範囲外を引数エラーにする契約なので、この確認を選びます。例外クラスの完全一致や表示文までが要件なら、確認内容もそれに合わせて追加します。

try-catchで例外をつかまえて何もせず終えると、例外が出なかったときまでテストが通る書き方になりかねません。「例外が必要」という条件そのものをJUnitへ渡すのが、この例でassertThrowsを選ぶ理由です。

pom.xmlは、JUnitの依存関係と実行方法を指定する

ZIPに含めたpom.xmlの全文です。ダウンロードした場合は、貼り付け直す必要はありません。手でファイルを作る場合は、srcと同じ階層へ、この内容を保存してください。

<?xml version="1.0" ?>
<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>info.syachiku.samples</groupId>
  <artifactId>junit-first-test</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-resources-plugin</artifactId>
        <version>3.2.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.6</version>
      </plugin>
    </plugins>
  </build>
</project>

junit-jupiterはテストを書くAPIと実行エンジンを用意する依存関係です。scopeのtestは、テスト用に使う指定です。maven.compiler.releaseの17は、コンパイルするJavaの対象を指定します。maven-surefire-pluginは、mvn testでテストを実行する側です。

ここで丸暗記する必要があるのはXMLの並びではありません。「JUnitの名前が解決できないなら依存関係」「テストが実行されないなら配置・名前・実行設定」というように、どの役割の設定を見るかを分けるために使います。

pom.xmlのある場所でmvn testを実行し、3件成功を確かめる

ZIPを展開したjunit-first-testフォルダをターミナルで開きます。srcの中ではなく、pom.xmlが見える場所です。次のコマンドで、いま開いているShippingFeeTest.javaの3件を実行します。

mvn test

Mavenが本体とテストをコンパイルし、JUnitのテストを実行します。初回に依存関係のダウンロード表示が続いても、最後の結果まで待ちます。今回の3件が実行されると、結果には次の行が出ます。

[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Tests run: 3で3件が実行され、Failures: 0とErrors: 0で失敗がないと分かります。Skipped: 0は飛ばされたテストがないことです。BUILD SUCCESSだけを見ず、件数も確認してください。実行対象が見つからない状態を、送料の仕様が正しいと確認できた状態と取り違えないためです。

確認対象は、このJavaメソッドの入力・戻り値・例外です。Web画面への表示、DBへの保存、注文全体の金額計算まで成功したわけではありません。実務で報告するときも、「送料計算のこの3件を実行した」と範囲を伝えます。

10,000円のテストだけ失敗させ、expectedとbut wasを読む

次にShippingFee.javaの条件を、subtotal >= 10_000からsubtotal > 10_000へ、一時的に変更してください。変更するのは本体のこの1か所です。ShippingFeeTest.javaの期待値は変えません。

// 一時的に、この条件へ変更する
return subtotal > 10_000 ? 0 : 500;

もう一度mvn testを実行すると、10,000円のテストが失敗します。以下は今回、条件を変えて実行した結果の抜粋です。時間やJUnit内部の行番号は環境で変わるので、ここでは失敗名と値を見ます。

[ERROR] ShippingFeeTest.subtotal10000IsFree
org.opentest4j.AssertionFailedError: expected: <0> but was: <500>
    at ShippingFeeTest.subtotal10000IsFree(ShippingFeeTest.java:15)
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE
期待0円に対して実際500円となった10,000円のケースを、以上なら無料という仕様へ戻って確認。無料範囲に10,000円ちょうどを含むよう、本体の比較を大なりから以上へ直す図。
期待値を書き換える前に、入力・仕様・本体の条件を照合します。

expected: <0>は、テストに書いた期待値が0円であること。but was: <500>は、本体が実際に500円を返したことです。左が正しいと自動判定されたわけではなく、まず「テストに書いた約束と、実際の結果が違う」と分かった状態です。

ShippingFeeTest.subtotal10000IsFreeが失敗したテスト名です。詳細にはShippingFeeTest.java:15も出ます。配布したテストの15行目、assertEquals(0, shippingFee.calculate(10_000));へ戻ってください。ここで本体へ渡している入力は10,000、期待する送料は0です。JUnit内部のクラスではなく、自分のテストから読みます。空行を増やした場合、行番号は変わります。

そこから本体のcalculateを確認します。>では10,000円ちょうどを無料にできません。仕様は「10,000円以上」なので、今回直すのは本体の比較です。期待値を500に変えれば緑になりますが、それでは誤った送料を正解として固定してしまいます。

レビューで説明するなら

「仕様は10,000円以上で送料無料です。10,000円のケースで、期待0円に対して500円になりました。本体が>で判定していたため、>=へ戻しました。3件を再実行し、すべて成功しています。」

これは今回の練習に対する説明例です。「テストが赤いから直した」ではなく、要件と実際の差、直した場所、再確認した範囲を伝えます。もし業務の仕様自体が未確定なら、どちらの値が正解かを先に確認します。

条件を>=へ戻し、テストを全件実行する

本体をsubtotal >= 10_000へ戻して保存し、再度mvn testを実行します。再びTests run: 3, Failures: 0, Errors: 0, Skipped: 0とBUILD SUCCESSになることを確認してください。

10,000円の1件が通っただけで終わらせず、9,999円の送料500円と、負数の拒否も一緒に実行します。修正前にできていたことを壊していないか、同じコマンドで確かめられるのが、テストコードを残す利点です。

テストは、本体の条件式をそのまま複製するより、9_999 → 500、10_000 → 0という入力と期待値を独立に残します。本体と同じ比較式で期待値を計算すると、今回のような間違いを両方へコピーする可能性があるためです。

テストが動かないときは、配置・設定・失敗を分けて見る

最初は、コードの結果が違う失敗と、実行する前に止まったエラーを区別すると直す場所を絞れます。赤い文字が出たからといって、すぐ送料の条件を変えないでください。

  • pom.xmlがないと言われる:実行場所を確認します。src/test/javaではなく、今回のpom.xmlのあるフォルダへ移動します。
  • org.junit.jupiter.apiが見つからない:pom.xmlにJUnitの依存関係があるか、初回の取得に失敗していないかを見ます。IDE側だけの赤線なら、Mavenプロジェクトとして読み直した上で、ターミナルのmvn testでも確認します。
  • テスト0件になる:ファイルがsrc/test/javaにあるか、名前がShippingFeeTest.javaか、importと@Testが本文どおりかを確認します。過去に変更した実行設定がないかも見ます。
  • expectedとbut wasが違う:テストは実行されています。失敗したテストの入力・期待値を仕様と照合し、それから本体を確認します。

assertThrowsで「例外を期待したが何も起きなかった」と出た場合も、実行自体はできています。今回なら入力が-1になっているか、本体の範囲チェックを消していないかを見ます。結果を緑にするために例外確認を削ると、負数を拒否する約束も確認できなくなります。

3件を動かせたら、入力範囲を確かめる9件へ進む

ここまでで、配置・実行・失敗の読み方は一通り試せました。次は、テストの件数を何となく増やすのではなく、まだ確認していない約束を探します。今回なら「0円を受け付ける」「100万円を受け付ける」「100万円を超えたら拒否する」という入力範囲が残っています。

発展用のサンプルには、入力範囲と送料が変わる境目を確かめる9件があります。冒頭で配布した記事用サンプルは3件、ここから先の発展版は9件です。3件を動かす練習が終わってから、追加された入力と期待値を比較してください。

発展:入力範囲も確かめる9ケース版

Java 17・Maven 3.9系・JUnit 5.12.2。ソース、テスト、pom.xml、実行手順を含みます。リンク先は固定版です。冒頭で配布した3件版とは別の発展用サンプルです。入力範囲と境界の確認を加えています。

GitHubで9件のテストと実行手順を見る

ブラウザでコードを読めます。実行する場合はリンク先のREADMEの手順に従います。SpringやDBの準備は不要です。

別の処理にテストを付ける段階で「どの入力を選ぶか」が決まらなければ、仕様を入力・操作・期待結果へ分ける方法を使ってください。次の記事では、今回のJUnitの設定ではなく、ケースを選ぶ理由を具体化します。

関連記事テストケースの作り方|仕様から正常系・境界値・異常系を出す例 次にどの入力を試すか迷ったときに。仕様を入力・操作・期待結果に分け、ケースを選ぶ理由を整理します。 記事を読む

提出前に、実行件数と修正理由を確認する

  • 配布したShippingFee.javaとShippingFeeTest.javaを開き、テストが本体のcalculateを呼ぶ場所を説明できる。
  • mvn testで3件が実行され、失敗・エラー・スキップが0になった。
  • 9,999円の期待値500と10,000円の期待値0を、送料の仕様から説明できる。
  • >=を>へ変えたとき、どのテストが落ちるかを実際に確認した。
  • 失敗したとき、期待値を実際の結果に合わせず、仕様・入力・本体を照合した。
  • 今回確認できた範囲と、まだ確認していない入力範囲を区別できる。

「なぜその入力を選ぶのか」から学び直したい場合は、『ソフトウェアテストの教科書[増補改訂 第2版]』が次の1冊です。JUnitのコマンドを覚えるためではなく、仕様から入力と期待結果を選ぶ考え方を補うために紹介します。

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

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

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

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

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

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

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

関連記事Javaのテスト入門におすすめの本|テストケースを選べる最初の1冊 JUnitを動かせた後、何を確かめるべきかを本で学びたい人へ。ケースを選ぶための最初の1冊を紹介します。 記事を読む 関連記事JUnitで境界値テストをまとめる方法|ParameterizedTestとCsvSourceの使い方 同じ処理に入力と期待値を何組も渡したくなったら。今回の@Testを理解した後、重複を減らして実行する方法へ進めます。 記事を読む

JUnitでは、失敗した値から仕様と本体へ戻る

本体をsrc/main/java、テストをsrc/test/javaへ置き、pom.xmlのある場所でmvn testを実行する。戻り値はassertEquals、必要な例外はassertThrowsで確認する。この手順で、送料の3件を繰り返し実行できます。

失敗したら、テスト名とexpected/but wasから、入力と期待結果を確認します。今回の10,000円は「以上」という仕様に戻ることで、直すべき場所が本体の比較だと判断できました。緑にする操作だけでなく、何の約束を戻したのかまで説明することを、最初の到達点にしてください。

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