JUnit ではじめる単体テスト入門 研修コースに参加してみた

今回参加したコースは JUnit ではじめる単体テスト入門 です。
以前にレポートした 単体テストとテストケースの作り方 研修コースに参加してみた の拡張版コースで、品質計画から、テストの考え方、単体テスト(ユニットテスト)をおさらいして、 JUnit をじっくり 1 日かけて演習しました。
このレポートではそのうち、 JUnit を使った演習の部分を中心にレポートします。
なお、このコースはオンラインで開催されました。
もくじ
コース情報
想定している受講者 | Java 言語経験者 |
---|---|
受講目標 |
|
講師紹介
このコースで登壇されたのはクロノスのお馴染み 藤丸 卓也 さん です。
label藤丸さんが登壇した以前のコース

今回のコースでは、オンライン研修でありながら、白紙の PowerPoint をホワイトボード代わりに、その場で図を描きながら説明するという工夫もされていて、わかりやすく理解できました。

講師紹介とあわせて、今日のコースでやることを紹介いただきました。
- 今日の進め方
- 前半は講習
- 後半は演習。テストコードを書いていく
- 単体テスト
- 新入社員がはじめてやることが多い
- 学ぶ機会も少ない
- JUnit で自動テストしてみよう
- テストケースや回数は感覚的になりがち
品質計画
前出の 単体テストとテストケースの作り方 研修コースに参加してみた と重複する部分は割愛し、新しく解説いただいたところを紹介します。
ポイントは単体テストは機能要件のテストであること。
- 設計 -> 実装の前にテストケースを書こう(テスト設計)
- それをレビューしてもらおう(抜け漏れなどが防げる)
- 設計書のバグもチェックできる
- レビューが通れば実装開始
単体テスト
- 現場では「単テ」「UT(ユーティ。Unit Test の略)」とも呼ばれる
- スタブ ( Stub ) とモック ( Mock ) の違い
- スタブは依存するモジュールからデータをもらいテスト
- モックは依存するモジュールを呼んでいるかどうかをテスト
単体テストのやり方
- ホワイトボックステストの C2
- ホワイトボックステストはプログラムの構造がわかっている
- C2 は条件網羅のテスト
- すべての条件式をテストする
- 条件式ごとの命令も 1 回はテストする
- C0, C1 ではなく C2 が一番多い
- ブラックボックステスト
- 入出力でテストする。プログラムの中身は知らない
- 同値分割
- 有効な値、無効な値から代表値を決めてテスト
- これではテスト不足
- 境界値分析
- 有効/無効の境界の値でテストする
- デシジョンテーブル
- 入力と出力の条件の組み合わせが多い場合、表のようなもので現す
- ex. 駐車場料金のディスカウント
- 3000 円以上のお買い物で 1 時間無料
- 5000 円以上のお買い物で 2 時間無料
- :
- 映画なら 3 時間無料
- 入力と出力の条件の組み合わせが多い場合、表のようなもので現す
- エラー推測
- 異常系のテスト不足を補う
- エラーが発生しそうなデータパターンでテストする
- int であれば最大値、小数点など
- string なら空文字、スペース、 null など
- date なら閏年、存在しない日付・時刻
- テストケースのチェックシート
- 上記のようなものを含め、予めテストしたい項目を共有しておくと抜け漏れがなくなる
JUnit とは
ということで、いよいよ JUnit です。
まずは JUnit について紹介いただきました。
- JUnit
- Java で動作するテスト自動化フレームワーク
- JUnit 5 を使います
- 従来 JUnit 4 を使っていたが IDE で警告がでるため、切り替え
- 単体テスト自動化だけじゃなく、拡張機能を使うと、結合テストまでカバーできる
- Selenium で結合テストまで出来る
- 自動テストとは
- 開発したものが意図通りに動く
- 処理を実行して、テスト結果の検証をプログラム(テストコード)で行う
- ヒトが介さない
- 自動テストのメリット
- 何度でもスグに実行できる
- 変更に強くなる
- テストしやすい = 疎結合
- 故障に強くなる
- 仕様を永続化できる
- テストコードが仕様
- 属人性が排除できる
JUnit テストケース作成
ここから演習です。
演習は、仮想デスクトップにログインして、 Eclipse IDE から、用意されている演習サンプルを開いて行います。サンプルは、 Gradle を使ってビルドしています。
- test コードと product コードが分かれたディレクトリ構成
- 別に resource (画像やファイル) のディレクトリが分かれている
- 対象プログラムの仕様
package secollege.junit.example;
public class Login {
boolean authenticate(String userId, String password) {
// 本来ならDB参照する
if ("user01".equals(userId) && "passw0rd".equals(password)) {
return true;
}
return false;
}
}
1. テストクラスの作成
- テスト対象のクラスから GUI でテストクラスを作成できる
- テストクラスの名前は ‘テスト対象クラスの名前’ + ‘Test’ eg. LoginTest
- 自動でスケルトンができる
package secollege.junit.example; import static org.junit.jupiter.api.Assertions.*; class LoginTest { @Test void test() { fail("Not yet implemented"); } }
@Test
というアノテーションが無いと、テストとして認識されない
2. テストケース (テストメソッド) を作成
- 一般的にテストケース 1 つにテストメソッド 1 つ
- 今回は クラス に対して、テストクラス
- Eclipse などを使っていると、自動補完機能があるので、それを利用
- Windows / Eclipse の場合、 Ctrl + space キーで表示
- Windows / Eclipse の場合、 Ctrl + space キーで表示
class LoginTest {
@Test
void testName() throws Exception {
}
}
3. テストコードを書く
メソッド名に日本語が使えるので、日本語で書いておくとわかりやすいでしょう。
class LoginTest {
@Test
void 有効なユーザーの場合認証成功() throws Exception {
}
@Test
void 無効なユーザーの場合認証失敗() throws Exception {
}
- カテゴリ ( given / when / then ) に分けて書くとよい
- 検証コードを書くときにはアサーションを使う
- 検証コードを書くときにはアサーションを使う
- 変数名に expected (期待値) や actual (実行結果) などわかりやすい命名ルールをつけよう
class LoginTest {
@Test
void 有効なユーザーの場合認証成功() throws Exception {
// given: 検証するデータと期待値を書く
String userId = "user01";
String password = "passw0rd";
boolean expected = true;
// when: テスト対象の実行コードを書く
boolean actual = new Login().authenticate(userId, password);
// then: 検証コード
assertEquals(expected, actual); // アサーション
}
}
実は 1 行でも書けますが、 ↑ のように書いておくと、わかりやすいとのこと。 ちなみに 1 行で書いた例を紹介いただきました。
assertEquals(true, new Login().authenticate("user01", "passw0rd"));
4. テスト実行
- GUI から実行してみます (ショートカットでも OK )
- Failure Trace に何もなければエラーなし
- テストメソッドが日本語だと何のテストをしたのかわかりやすい
- エラーがあった場合
- 成功するメソッドの userId を変えてみる
JUnit の拡張を使ってみよう
- 例外
- パラメータ化テスト
- 構造化テスト
JUnit の拡張プラグインを入れると、いろいろ便利に使えます。
今回はこの 3 つを使ってみます。
例外処理
対象のプログラムに仕様追加をします。
package secollege.junit.example;
public class Login {
boolean authenticate(String userId, String password) {
// ユーザIDの入力値チェック
if (userId == null) {
throw new IllegalStateException("ユーザID未設定");
}
if ("koizuss".equals(userId) && "passw0rd".equals(password)) {
return true;
}
return false;
}
}
- assertThrows アサーションが JUnit 5 から使えるようになった
assertThrows(例外の型, テスト対象の実行コード)
- 例外の型に注意。今回は
IllegalStateException.class
class まで書く - 第 2 引数の実行コードはラムダ式 (無名関数) で書く
() -> new Login()
- 例外の型に注意。今回は
- 実行すると例外が発生するので、 when と then が一緒になる
class LoginTest {
// 中略
@Test
void ユーザIDが未設定の場合IllegalStateException発生() throws Exception {
// given
String userId = null;
String password = "passw0rd";
// when-then
assertThrows(IllegalStateException.class, () -> new Login().authenticate(userId, password));
}
}
実行してみます。
パラメータ化テスト
- 例えば、文字入力ボックスがあったとして、 null など空文字、大文字小文字など様々な値が入る
- いちいちそれをメソッドに書くときりがない
- この入力値をパラメータ化することで、 1 つのメソッドで実行できる
対象のプログラムに仕様追加をします。
package secollege.junit.example;
public class Login {
boolean authenticate(String userId, String password) {
if (userId == null || userId.length() == 0 || " ".equals(userId) || " ".equals(userId)) {
throw new IllegalStateException("ユーザーID未設定");
}
// 中略
}
}
@ParameterizedTest
というアサーションを使う- つづいて、
@ValueSource
という引数を定義- ただし null は検証できない ->
@MethodSource
などを使う
- ただし null は検証できない ->
class LoginTest {
// 中略
@ParameterizedTest
@ValueSource(strings = { "", " ", " " })
void ユーザーIDが空文字またはスペースの場合IllegalStateException(String value) throws Exception {
// given
String userId = value;
String password = "passw0rd";
// when-then
assertThrows(IllegalStateException.class, () -> new Login().authenticate(userId, password));
}
}
実行してみます。
構造化テスト
- テストクラスをネスト (入れ子、構造化)できる
- クラスの中にクラス
- このあと紹介する DBUnit などで使う
- 例えば、初期化の処理が違ったりする場合
- テスト対象のプログラムは変更なし
- テストクラスを構造化する
- 認証を行うクラス
- 入力値チェックを行うクラス
class LoginTest {
@Nested
class 認証 {
void 有効なユーザーの場合認証成功() throws Exception {
// 処理
}
void 無効なユーザーの場合認証失敗() throws Exception {
// 処理
}
}
@Nested
class 入力値チェック {
@ParameterizedTest
@ValueSource(strings = { "", " ", " " })
void ユーザーIDが空文字またはスペースの場合IllegalStateException(String value) throws Exception {
// 処理
}
}
}
実行してみます。
ネストをどこまで OK にするかは、単体テストの粒度にも関わりそうですね。
そのほか便利な拡張機能や外部ツール
よく使われる拡張を 3 つ使ってみましたが、それ以外にも便利なものを紹介いただきました。
DBUnit を使ってみよう
- 実際には JUnit 単体では使わず、基本は拡張ライブラリを入れて使う
- DBUnit はデータベースの操作(ex. CRUD )を設定できる
- ローカルで開発していると、開発者ごとにデータベースに入っているデータが違う
- 結果が変わってしまうので、ツラい
- 投入データには XML / CSV / Excel を使える
- 今回は XML を使う
テスト仕様は ↓ です。
- 対象プログラム
- Dao: DB操作を行うクラス
- Dto: 操作して得られた結果を保持するクラス
- データベースの状態
+-----------------------+ | Tables_in_fruits_shop | +-----------------------+ | fruits | +-----------------------+
- 投入するデータ
<?xml version="1.0" encoding="UTF-8"?> <dataset> <fruits id="1" name="Apple" price="100" /> <fruits id="2" name="Banana" price="200" /> <fruits id="3" name="Cherry" price="300" /> </dataset>
もちろん処理に合わせて、投入データを変えられます。
長いので、かなり省略して書きます
package secollege.dbunit.example.dao;
import static org.junit.jupiter.api.Assertions.assertEquals;
// 中略
import org.dbunit.Assertion;
// 中略
import secollege.dbunit.example.dto.FruitsDto;
class FruitsDaoTest {
private static final String DB_SCHEMA = "fruits_shop";
// (中略)
@BeforeAll
static void 全体の前処理() throws Exception {
// DB接続情報の取得など
}
@AfterAll
static void 全体の後処理() throws Exception {
// リソースの破棄
}
@Nested
class SELECTの検証 {
@BeforeEach
void 前処理() throws Exception {
// XMLに定義している情報をデータセットとして取得して、既存データを削除 -> 登録
}
/**
* findAllメソッドのテスト
* @throws Exception
*/
@Test
public void Fruitsテーブルからデータを全件取得する() throws Exception {
// データの全件取得や取得件数の検証
}
}
@Nested
class INSERTの検証 {
@BeforeEach
void 前処理() throws Exception {
// XMLに定義している情報をデータセットとして取得して、既存データを削除 -> 登録
}
/**
* createメソッドのテスト
* @throws Exception
*/
@Test
public void Fruitsテーブルにデータを登録する() throws Exception {
// データセットを取得 -> テーブル定義を取得 -> データ登録 -> 取得
// データの検証
Assertion.assertEquals(expectedTable, actualTable);
}
}
}
投入するデータを追加して実行しみましょう。
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<fruits id="1" name="Apple" price="100" />
<fruits id="2" name="Banana" price="200" />
<fruits id="3" name="Cherry" price="300" />
<fruits id="3" name="Dragon Fruits" price="400" />
</dataset>
実行してみます。
ちなみに、ローカルのデータが変わってしまうのがツラい場合は、バックアップの処理を書いたり、seed の処理を書くとよいでしょう、とのことでした。
Mockito (モキート) を使ってみよう
- スタブとモックの違いで説明したように、依存するモジュールが出来てない場合などに使う
- 先の Dao クラスが行っていた処理をモック化してみましょう
- Mockito というライブラリを使います
- 対象プログラム
- 先程の Login
- ただし、依存する userDao クラスは完成していない
package secollege.junit.example;
import secollege.junit.example.dao.UserDao;
public class Login {
UserDao userDao = UserDao.getInstace();
boolean authenticate(String userId, String password) {
// 中略
// ここが今回追加したところ。DBに接続して、IDとパスワードを取得する
if (userDao.getUser(userId, password) != null) {
return true;
}
return false;
}
}
では、モック化してみましょう
- user.Dao をモック化
- モックが返すデータを定義
- user.Dao の getUser メソッドをモック化
// 中略
import org.mockito.Mockito;
// 中略
class LoginTest {
@Nested
class 認証 {
@Test
void 有効なユーザーの場合認証成功() throws Exception {
// given
// テスト対象クラスのインスタンス化
Login login = new Login();
// Dao オブジェクトをモック化
login.userDao = Mockito.mock(UserDao.class);
// モックが返すユーザーデータを定義
User user = new User();
user.id = userId;
user.password = password;
// userDao.getUser をモック化
Mockito.when(login.userDao.getUser(userId, password)).thenReturn(user);
// when
boolean actual = login.authenticate(userId, password);
// then
assertEquals(expected, actual);
}
@Test
void 無効なユーザーの場合認証失敗() throws Exception {
// 中略
}
}
// 中略
}
実行してみます。
Selenium を使ってみよう
- 結合テストのときに使う
- 具体的にはブラウザの操作をプログラムで書ける
- Selenium はブラウザの操作ライブラリ
- Selenide はブラウザの操作テストライブラリ
- エラーのスクショを自動で撮ったりできる
- 今回のテスト
今回はブラウザで直接テストするので、対象プログラムはありません。早速、テストクラスを見てみましょう。
// 中略
// なお、 DBUnit なども同じだが、 Gradle などのビルドツールの設定ファイルにも書く
import org.openqa.selenium.By;
import com.codeborne.selenide.Condition;
// 中略
// chrome を使うときはドライバが必要。Firefox は必要なし
import com.codeborne.selenide.WebDriverRunner;
class GoogleSearchTest {
@BeforeAll
static void setup() {
// WebDriverの設定
Configuration.browser = WebDriverRunner.CHROME;
System.setProperty("webdriver.chrome.driver", "C:\\chromedriver.exe");
}
@Test
void Googleトップページで検索() throws Exception {
// Googleトップページを表示
Selenide.open("https://www.google.com");
// 検索ボックスの要素取得
// Selenide.$ : 画面上の要素取得(jQueryのようにセレクタ指定ができる)
SelenideElement element = Selenide.$(By.name("q"));
// 検索ボックスに“JUnit"と入力して検索
element.val("JUnit").pressEnter();
// 検索結果の取得
// Selenide.$$ : 複数要素を配列で取得
ElementsCollection searchResults = Selenide.$$(".LC20lb");
// 検索結果の検証
searchResults.get(0).shouldHave(Condition.text("JUnit - About"));
}
}
実行してみましょう!
CI / CD
今回は触れられなかったんですが、自動テストをフックに DX ( Developer Experience 開発者体験) 向上にも繋がります。
- GitHub などリポジトリサーバからプッシュ
- Jenkins など CI ツールが以下を実行
- 自動でビルドツールが走る
- 自動でテスト実行
- 結果を Redmine などバグトラッカーに登録
テスト自動化をやるのかやらないのか
テスト自動化は便利だけど、初期コストがかかるので、その損益分岐を考えることが重要、というお話です。
- 自動テストを書く工数がかかる
- テストをメンテナンスする工数がかかる
- 品質が必ず上がる訳ではない
開発期間が 3 ヶ月を超える、開発規模が大きいなどの場合は、初期コストを回収しやすくなります。
このため、
- やっても無理をしない(初期コストを肥大化させない)
- ただ、やるなら早めにやる
- あとからやるのは大変。。テストコードを書いているうちにプロダクトコードは拡大する
- やるなら、テストコードを先に書こう
このような心がけが必要です。
いまの DX (デジタル・トランスフォーメーション) のような開発には、アジャイル開発はほぼ必須なので、やってみましょう、とのことでした。
まとめ
このコースでは、 JUnit による単体テストについて、実践をまじえて学びました。
JUnit の使い方の基礎だけでなく、テストメソッドの名前や、メソッドの内部の整理方法、アンチパターンなど、テストケースを書くうえでの心得まで解説されました。また、演習の例題においても、よく使われる拡張や外部ツールとして DBUnit, Mockito, Selenium などを解説するなど、実践を意識して解説されました。
ここでレポートしたほかにも、品質基準や品質計画、あるいはどのようにテストすべきなのかなど、テストと品質に関して全般が語られました。
JUnit でテストを書いたことがない方や、何をどうテストしたらいいか自信がない方など、テストに悩んでいる方におすすめのコースです。

SEプラスにしかないコンテンツや、研修サービスの運営情報を発信しています。