テスト対象の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.javasrc/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 testMavenが本体とテストをコンパイルし、JUnitのテストを実行します。初回に依存関係のダウンロード表示が続いても、最後の結果まで待ちます。今回の3件が実行されると、結果には次の行が出ます。
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESSTests 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
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件版とは別の発展用サンプルです。入力範囲と境界の確認を加えています。
ブラウザでコードを読めます。実行する場合はリンク先のREADMEの手順に従います。SpringやDBの準備は不要です。
別の処理にテストを付ける段階で「どの入力を選ぶか」が決まらなければ、仕様を入力・操作・期待結果へ分ける方法を使ってください。次の記事では、今回のJUnitの設定ではなく、ケースを選ぶ理由を具体化します。
提出前に、実行件数と修正理由を確認する
- 配布した
ShippingFee.javaとShippingFeeTest.javaを開き、テストが本体のcalculateを呼ぶ場所を説明できる。 mvn testで3件が実行され、失敗・エラー・スキップが0になった。- 9,999円の期待値500と10,000円の期待値0を、送料の仕様から説明できる。
>=を>へ変えたとき、どのテストが落ちるかを実際に確認した。- 失敗したとき、期待値を実際の結果に合わせず、仕様・入力・本体を照合した。
- 今回確認できた範囲と、まだ確認していない入力範囲を区別できる。
「なぜその入力を選ぶのか」から学び直したい場合は、『ソフトウェアテストの教科書[増補改訂 第2版]』が次の1冊です。JUnitのコマンドを覚えるためではなく、仕様から入力と期待結果を選ぶ考え方を補うために紹介します。
ソフトウェアテストの教科書[増補改訂 第2版]
仕様から、試す入力と期待する結果を選べるようになる。
同値分割・境界値や条件の組合せなど、テストケースを選ぶ考え方を基礎から学ぶ1冊。何を試せばよいかわからない段階で選びます。
- 正常な値を1件試した後、追加するケースに迷う
- 入力と期待値を選んだ理由を説明したい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
JUnitでは、失敗した値から仕様と本体へ戻る
本体をsrc/main/java、テストをsrc/test/javaへ置き、pom.xmlのある場所でmvn testを実行する。戻り値はassertEquals、必要な例外はassertThrowsで確認する。この手順で、送料の3件を繰り返し実行できます。
失敗したら、テスト名とexpected/but wasから、入力と期待結果を確認します。今回の10,000円は「以上」という仕様に戻ることで、直すべき場所が本体の比較だと判断できました。緑にする操作だけでなく、何の約束を戻したのかまで説明することを、最初の到達点にしてください。



