165
160

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JUnit未経験者がテストコードを書ききるためにまずやったことと、初心者的悩みポイントを逆引き解説

Last updated at Posted at 2022-07-03

はじめに : ユニットテストを書いたことない頃の私の声

わかっているんです。知っているんです。ユニットテストを書いた方がいいのは。

だけどさ何から始めればいいの?
どうやってテストコード追加するの?
なんかアノテーションがやたらあるのは知ってる。ただ使い方がわからん。
ていうか時間がない!書く時間というか、そもそも最初のキャッチアップの時間が!作れない!

というのは数か月前までの私の心の声。

その後、ユニットテストを2つほど書いてみたところで、とりあえず上記のような心の声には「大丈夫だよ、これで始められるよ」と寄り添えるようになった気がします。
そこで

  • ユニットテストナニモワカラナイ(本来の意味)・・から、どきどきしつつも挨拶ができるようになるくらいまでに何をしたか
  • テストを書くにあたって最初に直面した「これってどうしたらいい・・・?」を逆引き解説※

をまとめます。

※解説とか大層なことを言っていますが、初心者です。間違っているところや「もっとこう書いたらいいよ!」ということがあると思うのでぜひご意見お願いします

環境

とにかくまずやったこと(所要時間合計:約4時間)

【まずやったこと①】準備運動:ライブコーディング動画を写経し、自分でユニットテストを書くための雰囲気をつかむ(3時間)

テストの書き方がとにかく全くわからないので、なにはともあれテストコードを書く流れを知ることが第一。そのためにはテストを書ける人がどう書いているのかを知るのが1番。
ということで私が最初に参考にしたのがこちら。

FizzBuzz問題とQ&Aのアーカイブ機能の開発というもう少し実践的な実装を、TDD(Test-Driven Development:テスト駆動開発)でライブコーディングしていく動画です。
コーディングだけでなく、テストコードを書く重要性、TDDのメリットや課題などの理論的な部分なども含めて説明してくれます。

こちらのライブコーディングの内容を写経しつつ、ここで@Test@ParameterizedTest@CsvSourceなどのアノテーションの存在を知りました。
自分がテストを書きたいなと思っていたメソッドが、2つの設定値の組み合わせパターンで返り値が変わるというものだったので、この辺り使えそうだなーなどとあたりをつけるのにも参考になりました。

なお、私はすでに社内で用意されていたJUnit導入済みのプロジェクトを利用して始めてしまいましたが、あらかじめこちらなどを参考にしてJUnit5を導入したうえで写経するのがおすすめです。

そもそも自動テストとは?TDDとは?といった説明部分は1.5倍速などで聴きつつ、コーディング部分は一時停止しつつでやって大体2時間半ほど。

@t-wadaさんによるライブコーディング。
先に1つめの動画を見たので2つめに関しては準備運動当初はつまみ食いしただけですが、テストを書き起こすためのパターンの整理方法やTDDのサイクルやスキルの話などとても参考になりました。1

【まずやったこと②】動画内で存在を知ったアノテーションやテストメソッドについてかるーく確認(30分)

実際は【まずやったこと①】と並行でやりましたが、ライブコーディングで出てきたアノテーションやメソッドを下記のサイトで検索して調べました。。

v5.3.0時点の公式サイトの日本語訳なので最新の情報ではないですが、概要を掴むのには敷居が低くてよかったです。
英語版の最新の公式サイトと比較すると、アノテーション部分だけでも差分が出ているので、自プロダクトで利用しているJUnit5のバージョン次第では公式を見てください。

【まずやったこと③】テストクラスの作成(15分)

テスト対象のクラスを起点にテストクラスを作ります。

Eclipseのテストを書きたいと思ったクラスの上でショートカットCtrl + 9を入力します。2
このショートカットで、すでにテスト対象クラスに紐づくテストクラスが存在する場合は該当のテストクラスを、紐づくテストクラスがない場合はこのダイアログが開くのでYesを選択してください。
image.png
これでテストクラスの作成はほぼ完了です。

Tips: JUnit5でテストを書くときは "New JUnit Jupiter test"を選択

New JUnit Test Caseのダイアログが開いたのでJUnit5を選択しようと思ったけれど
image.png
あれ、New JUnit 5 testの選択肢がない・・?

と思ったら、こちらの記事によるとJUnit Jupiter:JUnit 5 でテストや拡張機能を作成するためのモジュールとのこと。なので3つ目のNew JUnit Jupiter testを選択すればOKです。
image.png
自分で一からセットアップしていれば「そりゃそうだろ」という感じですが、はじめてテストコードを書いたのは社内の既存プロジェクト。すでにJUnit5がビルドパスに組み込まれていたため基本的なことを知りませんでした・・・

Tips: テストクラス作成時のお作法

src/main/java/Hoge.javaというクラスに対してテストクラスを作る場合、

  • packageはsrc/test/java
  • 命名はHogeTest.java

とするのがお作法です。

また、テストクラスは、テスト対象クラスを継承するとテスト対象のメソッドを呼びやすいです(継承しなくてもOK)。

このあたりはEclipseがテスティングペア作成時によろしくやってくれるので、source folderやpackageなどをデフォルトのまま作成すれば問題ありません。

【まずやったこと④】まずはとにかくテストを書いてみる(15分)

まずやったことのでちらほらテスト用のアノテーションやメソッドが登場するので、最初から複雑なことを考えてしまいがちです。
が、まずはとにかくシンプルに。以下だけでとりあえずテストを書いてみました。

  • @Test : これはテストのためのメソッドですよ、と宣言するためのアノテーション。逆につけないとJUnitテストと認識されない
  • assertTrue(boolean condition) : conditionがtrueを返すかを確認できる
  • assertFalse(boolean condition) : conditionがfalseを返すかを確認できる

こんな感じで書いてみました。3

FunctionAuthManagerTest(テストクラス)
class FunctionAuthManagerTest extends FunctionAuthManager {
	@Test
	void withCommonConfigDependOnEmployee() {
		// テスト対象の`FunctionAuthManager#canManageEachConfigByEmployee`にEnumCommonConfig.DEPEND_ON_EMPLOYEEを渡したらtrueを返すことをテストする
		assertTrue(canManageEachConfigByEmployee(EnumCommonConfig.DEPEND_ON_EMPLOYEE, null));
	}
}

なお、テスト対象のサンプルコードとしてとあるサービスの利用権限を管理するためのクラス(FunctionAuthManager)を用意しました。業務上のソースコードとしても割とよくある内容かなと思います。

FunctionAuthManager(テスト対象クラス)
public class FunctionAuthManager {
	/**
	 * とあるサービスの利用設定をユーザー個人に管理させるか
	 */
	public boolean canManageEachConfigByEmployee(EnumCommonConfig config, CommonConfigProvider provider) {
		return switch (config) {
		case USE, UNUSE -> false;
		case DEPEND_ON_EMPLOYEE -> true;
		case UNDEFINED -> {
			EnumCommonConfig def = provider.getDefault();
			if (def == EnumCommonConfig.UNDEFINED) {
				System.err.println("Illegal default config value: " + def);
				yield false;
			}
			yield canManageEachConfigByEmployee(def, provider);
		}
		default -> {
				System.err.println("Unexpected value: " + config);
				yield false;
			}
		};
	}
}
EnumCommonConfig(利用設定のパターンを集めたenum)
public enum EnumCommonConfig {
	USE, UNUSE, DEPEND_ON_EMPLOYEE, UNDEFINED;
}

実際に手を動かしてみたい方へ

下記にサンプルソースを置いています。

配置しているもの

  • 空の状態のFunctionAuthManagerTestクラス
  • FunctionAuthManagerクラスをはじめとしたテスト対象のサンプルコード

この記事の中で各テストコードを同じように写経できるサンプルコードとなっています。
cloneし、junit5-first-hands-onブランチをcheckoutしてご自身の環境にインポートして写経してみてください。

GitHubからのcloneおよびブランチのチェックアウト方法

Git Bashなどで

  • git clone git@github.com:moromi25/junit5-sample.git
  • (junit5-sampleに移動)
  • git checkout junit5-first-hands-on

Eclipseへのインポート方法

Package Explorer上で右クリック > Import > General > Existing Projects into Workspace

【まずやったこと⑤】テストを実行してみる(5分)

テストクラス上で右クリック > Run As > JUnit Testで実行します。
image.png
無事にテストが通りました!

ちなみに、敢えてwithCommonConfigDependOnEmployeeの中でassertFalseを使ってテストを失敗させてみると、こんな感じで教えてくれます。
image.png

【まずやってみた所感】無事にテストは書けたけど・・・

よしテスト書けた!もう少し改良してみよう!と書き進めていく中で、こんな悩みや疑問が生まれました。

  1. 正直assertTrue/Falseだけじゃ話にならないわ。返り値はbooleanばっかりじゃないし
  2. 例外処理をテストしたい
  3. テスト実行結果、メソッド名だと正直内容わかりづらい
  4. テスト対象コードで使うクラスがテストに都合のいい返り値などを返すようにしたい
  5. テスト対象クラスにメソッドがいくつもあって、各メソッドにつきテストを複数書いていって・・・あーもうどこに何のテストが書いてあるか迷子だわ
  6. 複数のテストで共通の事前処理を、全テスト実行前に1度だけしたい
  7. あれ?privateメソッドのテストを書こうとすると怒られる・・・?
  8. テストメソッドを使いまわしたい
  9. staticメソッドをMockしたい

それぞれの悩み・疑問をもとに前述したテストコードを改良しつつ、その対処法について逆引き的に書いていきます。
これ以降は、必要なところをかいつまんで見てもらえればと思います。

あるいは、上記の疑問点を入れ込んだテストクラスのサンプルをGitHubに置いています。
ソースコードベースで見られれば十分です、という方は下記のmainブランチを見てみてください。

1. 正直assertTrue/Falseだけじゃ話にならないわ。返り値はbooleanばっかりじゃないし ⇒【assertThat ほか】

回答としては、JUnit5のAssertionのJavadocやHamcrestというライブラリから提供されているメソッドの解説に目を通してみるのが1番といえばそれまでです。

ただ、何がテストしたいかって、A(実測値)がB(予測値)の値と同じ/違うかを比較することが非常に多いのではないでしょうか。

その場合は、HamcrestのassertThatがおすすめです。4

最初に書いたassertTrue(canManageEachConfigByEmployee(EnumCommonConfig.DEPEND_ON_EMPLOYEE, null));というテストコードをassertThatを使って書き換えてみます。

// 同値比較での置き換え
assertThat(canManageEachConfigByEmployee(EnumCommonConfig.DEPEND_ON_EMPLOYEE, null), is(true));

// 非同値比較での置き換え
assertThat(canManageEachConfigByEmployee(EnumCommonConfig.DEPEND_ON_EMPLOYEE, null), is(not(false))));

個人的おすすめポイントとしては、

  • assert that canManageEachConfigByEmployee is trueと英語ライクに読めることに好感
  • 英語的に考えれば引数の順序が1.実測値, 2.予測値であると直感的にわかりやすい
  • 第二引数に指定するCoreMatcherという判定用メソッドが豊富
    • もちろんboolean値以外の比較も書ける
    • not()を使えば「~でない」テストも可能
    • Matcherクラス自体をカスタマイズできるらしい
  • 後述する8. テストメソッドを使いまわしたい ⇒【@ParameterizedTest】との親和性が高い
boolean値以外の比較例
// int同士の比較
assertThat(Integer.MAX_VALUE, is(2147483647));

// Listに要素が含まれているか(比較ではないけどこんなメソッドも用意されている)
assertThat(Arrays.asList("hoge", "fuga"), hasItem("hoge"));

ちなみに、JUnitが提供しているassertEqualsというメソッドもあります。
ただ、個人的には予測値と実測値がどちらだったか混乱します・・・

assertEquals
// 第一引数が予測値、第二引数が実測値
assertEquals(true, canManageEachConfigByEmployee(EnumCommonConfig.DEPEND_ON_EMPLOYEE, null));

以上のことから、2つの値の比較で主に利用するのはHamcrestのassertThatがいいなと感じています。

2. 例外処理をテストしたい ⇒【assertDoesNotThrow, assertThrows】

例外が発生しないことをテストしたい ⇒【assertDoesNotThrow】

条件分岐の中で異常値が来た場合、エラーは吐きつつも処理は止めたくない、という実装は割とあると思います。
そんな「ちゃんと例外発生していないよね」を確認するのにはassertDoesNotThrowメソッドを使います。

assertDoesNotThrow
boolean canManageByEmployee = assertDoesNotThrow(() -> canManageEachConfigByEmployee(UNDEFINED));
assertThat(canManageByEmployee, is(false));

例外が発生することをテストしたい ⇒【assertThrows】

逆に、例外が発生することをテストする場合はassertThrowsを使います。

assertThrows
assertThrows(IllegalArgumentException.class, () -> canManageEachConfigByEmployee(UNDEFINED));

3. テスト実行結果、メソッド名だと正直内容わかりづらい ⇒【@DisplayName

前掲の実行結果、ちゃんと読めばクラスのどのメソッドのテストなのかわかりますし、そのメソッドをたどればどんなテストか理解はできますが、結構面倒ですよね。
たとえば

	void withCommonConfigDependOnEmployee() {
		// ★テスト対象の`FunctionAuthManager#canManageEachConfigByEmployee`にEnumCommonConfig.DEPEND_ON_EMPLOYEEを渡したらtrueを返すことをテストする
		...

この★をつけたコメントが実行結果に表示されたら、どんなテストをしているのか一目でわかるのになと。

そんな時に使うのが@DisplayNameアノテーションです。文字通りそのテストの表示名称となります。
メソッドだけでなくクラスにもつけられます。

FunctionAuthManagerTest(テストクラス)
@DisplayName("設定権限に関するテスト")
class FunctionAuthManagerTest extends FunctionAuthManager {
	@Test
	@DisplayName("DEPEND_ON_EMPLOYEEを渡したらtrueを返すか")
	void withCommonConfigDependOnEmployee() {
		...

実行結果がこちら。ただのクラス名、メソッド名の羅列よりも見やすくなりました。
image.png

4. テスト対象コードで使うクラスがテストに都合のいい返り値などを返すようにしたい ⇒【mock】

たとえばユーザーによる設定がされていない場合、システムデフォルト値を取得して判定する処理、よくあると思います。こんな感じです。

FunctionAuthManager
	boolean canManageEachConfigByEmployee(EnumCommonConfig config, CommonConfigProvider provider) {
		return switch (config) {
		...
		case UNDEFINED -> {
			// DBからデフォルト値を取得し、デフォルト値ベースで再判定
			EnumCommonConfig def = EnumCommonConfig.find(provider.getDefault());
			yield canManageEachConfigByEmployee(def, provider);
		}
		};
	}
CommonConfigProvider
	private DefaultCommonConfigRepository repo;
	...
	/** DBからデフォルト値を取得 */
	public int getDefaultVal() {
		return repo.getDefaultVal();
	}

今回扱うCommonConfigProvider#getDefaultはDBアクセスを伴う処理の想定です・・・ん?テストでDB処理・・・?!JUnit上でどうやればいいの?

そんな時に有効な手段の1つが、Mockitoが提供するstaticメソッド、mockです。

もっく とは・・・?と最初は思うかもしれませんが、mockという英単語には「疑似、まねる」という意味合いがあります。
なので、DBアクセスしてデータを取得する本来の処理に似せたふるまいを、テストクラスの中で定義してあげる、といったイメージです。

実際どう書くの?

こう書きます(一例)。

  • モック化したいクラスのインスタンス化 : mock(モック化したいクラス);
  • あるメソッドの返り値を指定 : when(ふるまいを指定したいメソッド).thenReturn(指定の返り値);
FunctionAuthManagerTest
class FunctionAuthManagerTest extends FunctionAuthManager {
	/** デフォルト設定を返してくれるクラス */
	private CommonConfigProvider mockedDefaultConfigProvider;

	@Test
	@DisplayName("UNDEFINEDを渡したらデフォルト設定:UNUSEを使って再判定するか")
	void withCommonConfigUndefined_defaultUnuse() {
		// モック化したいクラスのインスタンス化
		mockedDefaultConfigProvider = mock(CommonConfigProvider.class);
		// モック化したクラス内のメソッドのふるまいを定義
		when(mockedDefaultConfigProvider.getDefault()).thenReturn(UNUSE);

		assertThat(canManageEachConfigByEmployee(UNDEFINED, mockedDefaultConfigProvider), is(true));
	}
}

これでCommonConfigProviderをモック化したmockedDefaultConfigProvider#getDefaultメソッドは実際の実装がどうなっていようと必ずUNUSEを返すようになります。

注意点としては、テスト中にモック化したクラスのメソッドがよろしく動作してくれるのは、あくまでもふるまいを定義したメソッドのみということです。
そのため、利用したいメソッドに関してはすべてふるまいを定義する必要があります。定義していない場合、テスト実行時にjava.lang.NoSuchMethodErrorが発生します。

そのほかふるまいの定義方法についてはこちらなどを参考にしてみてください。

Tips: mockとspy

モック化したクラスで利用したいメソッドすべてのふるまいを定義する必要がある、と記載しましたが、クラス内の一部だけモック化し、それ以外のメソッドは元のロジック通りの動きをさせたいということもあるかもしれません。

そんなときはmockではなくspyというメソッドを使うのがよさそうです。

こちらの記事によると、

mock() はインスタンスの非 static 且つ public のメソッドをすべて Mock 化します。
なので一部のメソッドを実装のまま使いたい場合には適しません。
spy() は明示的に指定したメソッドのみを Mock 化します。
Mock 化しないメソッドは実装通りのふるまいとなります。

とのことです。

Tips: mock化の方法

モックの初期化方法はmockメソッドを利用した方法のほかに、アノテーションを利用した方法があります。

詰まった箇所: mockを利用したテスト実行時にjava.lang.ExceptionInInitializerError

Java9以上を利用する場合に発生する可能性があります。
もしかしたらJava9以上を使っている方には釈迦に説法かもしれませんが、普段Java8を使っているので解決に時間がかかりました・・・

エラーログ抜粋
java.lang.ExceptionInInitializerError
	at org.mockito.cglib.core.KeyFactory$Generator.generateClass(KeyFactory.java:167)
	at org.mockito.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
	at org.mockito.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:217)
(省略)
	at test.main.FunctionAuthManagerTest$CanManageEachConfigByEmployeeTest.testWithCommonConfigUndefined(FunctionAuthManagerTest.java:61)
(省略)
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @1ed1993a
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
	at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
	at org.mockito.cglib.core.ReflectUtils$2.run(ReflectUtils.java:57)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:318)
	at org.mockito.cglib.core.ReflectUtils.<clinit>(ReflectUtils.java:47)
	... 147 more

解決方法(一例)

実行時にvm引数に--add-opens java.base/java.lang=ALL-UNNAMEDを追加
image.png

原因

Java9 で導入された JavaPlatform Module System によりリフレクションを利用するフレームワークでInaccessibleObjectException が発生する場合があるらしいです。

今回はモジュールシステムは利用していなかったので、実行時のvm引数に--add-opens構文で指定することで解消しました。
モジュールシステムを利用している場合はmodule-info.javaへの対応が必要です。

5. テスト対象クラスにメソッドがいくつもあって、各メソッドにつきテストを複数書いていって・・・あーもうどこに何のテストが書いてあるか迷子だわ ⇒【@Nested

テストクラスの中で子クラスを作ってグルーピングすることで迷子防止ができます。
その際、子クラスであることを定義するためにつけるアノテーションが@Nestedです。

今回は共通設定をコントロールするcanManageEachConfigByEmployeeというメソッドに関するテストと、実際にサービスへのアクセス権限をコントロールするcanUseServiceというメソッドに関するテストの大きく2つに分かれるので、それぞれに関連するテストを1つのクラスにまとめて@Nestedを付けました。

@DisplayName("設定権限に関するテスト")
class FunctionAuthManagerTest extends FunctionAuthManager {
	@Nested
	@DisplayName("共通設定に関するテスト")
	class CanManageEachConfigByEmployeeTest {
		@Test
		@DisplayName("DEPEND_ON_EMPLOYEEを渡したらtrueを返すか")
		void withCommonConfigDependOnEmployee() {
			...
		}
	}

	@Nested
	@DisplayName("共通設定×ユーザー設定を加味したアクセス可否テスト")
	class CanUseServiceTest {
		@Test
		@DisplayName("共通設定がUSEの場合trueを返すか")
		void withComAuthTypeUse() {
			...
		}
	}
}

実行結果
image.png
ソース、テスト実行結果いずれも@Nestedでまとめられたクラスごとにグルーピングされ、可読性がぐっと上がりました。

6. 複数のテストで共通の事前処理を、全テスト実行前に1度だけしたい ⇒【@BeforeAll

@BeforeAllアノテーションをメソッドにつけると、同一クラス内にあるテストクラス実行の最初に実行される事前処理を書いたメソッドですよ、と宣言できます。
今回の場合、1度だけ初期化し、その後は複数のテストメソッドで使いまわしたいmockedDefaultConfigProviderの初期化に使えます。

@BeforeAll
setup() {
	// モック化したいクラスのインスタンス化
	mockedDefaultConfigProvider = mock(CommonConfigProvider.class);
}

テストの実施前後に処理を記載するために付与できるアノテーションは、このほかに以下が用意されています。

  • @BeforeEach:クラス内の各テストメソッド実行前に都度実行される事前処理
  • @AfterAll:同一クラス内にあるテストクラス実行後、最後に実行される処理
  • @AfterEach:クラス内の各テストメソッド実行後に都度実行される事後処理

7. あれ?privateメソッドのテスト書こうとすると怒られる・・・? ⇒【@VisibleForTesting

privateメソッドのテストを書こうとテストクラスで呼び出したけどコンパイルエラーで怒られる・・・当然です。privateメソッドですから。テストクラスはテスト対象クラスを継承しているだけに過ぎません。テストクラスだからセーフとかはありません。

とはいえprivateメソッドにもテストを書きたい。そんなときは、privateメソッドをアクセス修飾子なしのパッケージプライベートメソッドに変えます。

それだけだとただ可視性が上がっただけになってしまうので、Guavaの@VisibleForTestingアノテーションをつけることで、後続の開発者にこのメソッドはprivateだったんだけどユニットテストのために可視性を上げてますよと伝えるためのマーキングになります。5

このアノテーションはテストコード側ではなく、テスト対象のメソッドにつけるものなので注意してください。

8. テストメソッドを使いまわしたい ⇒【@ParameterizedTest

この記事で最初に書いたテストですが、テストしたいメソッドの引数になり得るenumのパターンは4種類あるので、全パターン書こうとするとこうなります。

class FunctionAuthManagerTest extends FunctionAuthManager {
	@Nested
	@DisplayName("共通設定に関するテスト")
	class CanManageEachConfigByEmployeeTest {

		@Test
		void withCommonConfigDependOnEmployee() {
			assertTrue(canManageEachConfigByEmployee(EnumCommonConfig.DEPEND_ON_EMPLOYEE, null));
		}

		@Test
		void withCommonConfigUse() {
			assertFalse(canManageEachConfigByEmployee(EnumCommonConfig.USE, null));
		}

		@Test
		void withCommonConfigUnuse() {
			assertFalse(canManageEachConfigByEmployee(EnumCommonConfig.UNUSE, null));
		}

		@Test
		void withCommonConfigUndefined() {
			mockedDefaultConfigProvider = mock(CommonConfigProvider.class);
			// TODO パターン化
			when(mockedDefaultConfigProvider.getDefault()).thenReturn(UNUSE);
			assertThat(canManageEachConfigByEmployee(UNDEFINED, mockedDefaultConfigProvider), is(true));
		}
	}
}

最後のwithCommonConfigUndefinedは少し処理が複雑ですが、それ以外の3メソッドはenumとアサーションの真偽値の組み合わせの差はあれど、書かれているコードはほぼ同じなので何とかしてまとめたくなります。

そこで利用できるのが@ParameterizedTestです。@Testが1つのテストであることをマーキングするアノテーションであるのに対し、@ParametirizedTestはパラメータを持つことで1つのテストメソッドに対して複数のテストパターンを作って実行することができます。

@ParameterizedTestへパラメータを渡す方法はいくつかあり、いずれもアノテーションとその引数で指定します。

@CsvSource

テストメソッドに対して複数の引数を渡したい時に使えます。
今回の場合、enumと実行結果の真偽値という2つのパラメーターをメソッドに渡してテストしたいので、こんな感じで書きます。

@CsvSourceの利用方法
@ParameterizedTest
@DisplayName("EnumCommonConfigのパターンテスト(UNDEFINED以外)")
@CsvSource({
	"USE,                false", 
	"UNUSE,              false", 
	"DEPEND_ON_EMPLOYEE, true" 
})
void withCommonConfig(EnumCommonConfig config, boolean expected) {
	assertThat(canManageEachConfigByEmployee(config, null), is(expected));
}

3つのテストメソッドが1つのメソッドにすっきりとまとめられました!

メソッドの引数にenumを使用していると、暗黙の型変換を行ってくれます。賢いですね。
enumだけでなく、当然Stringやintといった値もCsvSourceに含むことができます。
enum以外の型を利用した@CsvSourceのサンプルはこちらを見てみてください。

@EnumSource

たとえばUSEとUNUSEのパターンだけでいえば期待結果も同じなため、パラメータ化したいのはEnumCommonConfigだけ、という場合は@EnumSourceが使えます。

@EnumSourceの利用方法
@ParameterizedTest
@DisplayName("EnumCommonConfigのUSE, UNUSEパターンテスト")
@EnumSource(value = EnumCommonConfig.class, names = { "USE", "UNUSE" })
void withCommonConfig(EnumCommonConfig config) {
	assertThat(canManageEachConfigByEmployee(config, null), is(false));
}

上記の例では、EnumCommonConfigの列挙子のうち、USEとUNUSEだけをテストするよと指定しています。

たとえばEnumCommonConfigの全列挙子でテストを回したいという場合は、これだけでOKです。

@ParameterizedTest
@EnumSource
void withCommonConfig(EnumCommonConfig config) {
	...

@ValueSource

今回は使いませんでしたが、さらに単純なintやString,Classなど1つの型のみでパラメータ化するのであれば、@ValueSourceを使うとよさそうです。

@MethodSource

ここまで紹介したやり方はパラメータがすべて静的な場合でしたが、何かしら処理の実行をした上でパラメータ化したい場合には@MethodSourceが使えます。

mock化したクラスを利用したUNDEFINEDパターンのテストも含めてすべて1つのテストに含めようとすると、どうしてもデフォルト値の取得クラスをmock化する箇所が複雑になってしまうため、こんな感じで@MethodSourceを使ってみました。

@MethodSourceの利用方法
	@ParameterizedTest
	@DisplayName("共通設定に関するテスト")
	@MethodSource({ "exceptUndefinedProvider", "undefinedProvider" })
	void withCanManageEachConfigByEmployee(EnumCommonConfig config, CommonConfigProvider provider, boolean expected) {
		assertThat(canManageEachConfigByEmployee(config, provider), is(expected));
	}

	static Stream<Arguments> exceptUndefinedProvider() {
		return Stream.of(
				arguments(USE, null, false),
				arguments(UNUSE, null, false),
				arguments(DEPEND_ON_EMPLOYEE, null, true)
		);
	}

	static Stream<Arguments> undefinedProvider() {
		return Stream.of(
				arguments(UNDEFINED, getMockedProvider(USE), false),
				arguments(UNDEFINED, getMockedProvider(UNUSE), false),
				arguments(UNDEFINED, getMockedProvider(DEPEND_ON_EMPLOYEE), true),
				arguments(UNDEFINED, getMockedProvider(UNDEFINED), false)
		);
	}
	
	static CommonConfigProvider getMockedProvider(EnumCommonConfig def) {
		CommonConfigProvider mockedDefaultConfigProvider = mock(CommonConfigProvider.class);
		when(mockedDefaultConfigProvider.getDefault()).thenReturn(def.getVal());
		return mockedDefaultConfigProvider;
	}

解説としてはこちらの記事がわかりやすいです。

Stream を返す static メソッドを用意し、@MethodSource("methodSourceProvider") として指定することで、メソッドで生成した値を引数に与えることがでます。

補足として、Argumentsインスタンスを作るのにはorg.junit.jupiter.params.provider.Arguments.argumentsというファクトリーメソッドが用意されているので、ここにパラメータとして渡したい要素を入れ込みます。
今回は3つのパラメータをテストメソッドに渡す必要があるのでそれらを列挙しています。

今回@MethodSourceに複数のファクトリーメソッドを指定していますが、当然1つだけ指定してもOKです。1つの場合は@MethodSource("exceptUndefinedProvider")という感じで書けます。

引数にファクトリーメソッド名がない場合は、テストメソッド(今回だとwithCanManageEachConfigByEmployee)というファクトリーメソッドがあるか探してくれるようです。

@MethodSourceで使用するファクトリーメソッドの命名規則

これという指定はなさそうですが、公式に倣ってhogeProviderとするのがよさそうです。

@Nested内で@MethodSourceを使ってエラーが発生したら ⇒【@TestInstance(TestInstance.Lifecycle.PER_CLASS)】

表題のとおり、@Nestedクラス内で@MethodSourceを使うと以下のエラーで怒られます。

org.junit.platform.commons.PreconditionViolationException: Cannot invoke non-static method

その際、@Nestedをつけたクラスに @TestInstance(TestInstance.Lifecycle.PER_CLASS) アノテーションをつけることで解決します。

Tips: @Test@ParameterizedTestの実行結果の見え方

@Testメソッドを複数書いたとき、メソッド間の関連性は当然わかりません。そのため、関連性がわかるようにするためには@Nestedクラスなどで別途グルーピングする必要があります。
image.png

@ParameterizedTestでは、パラメータ化したパターンが1行ずつ結果に表示されるので、@Nestedクラスでグルーピングしなくても関連性がわかりやすいです。
image.png

Tips: テスト対象メソッドで直接利用するわけではないが引数として渡した方がいい値 ⇒【期待結果、 パターン説明】

テストの期待結果(expected)

ここまでの@ParameterizedTestの例で、テストメソッドの引数にboolean expectedを渡しているものがいくつかあります(期待結果がbooleanなのは今回のサンプルの都合なので、当然intでもなんでもOKです)。

引数のパターンに応じて期待結果が変わるのは普通のことなので、テスト対象のメソッドで利用する値を渡すのと同様に、期待結果を渡すことでassertThatの第二引数を可変にできます。
assertThatの節で@ParameterizedTestとの親和性が高い、と書いたのはここが理由です。

テストパターンの説明(description)

String descriptionを渡すのもテスト実行結果の可読性を上げるのに有効です。
ソース内のdocとして有効なのは言うまでもありませんが、なぜコメントではなく引数として渡すのか。
expectedについてはアサーションで利用するために渡していますが、descriptionはテストメソッド内のどこでも利用しません。

では引数として渡す理由は何か。それは、テスト実行結果で利用するためです。
@ParameterizedTestの引数は実行結果に表示されるようになっているため、第一引数にdescriptionを入れておくことで@DisplayNameの代用として利用できます。

	@ParameterizedTest
	@DisplayName("共通設定に関するテスト")
	@MethodSource({ "exceptUndefinedProvider", "undefinedProvider" })
	void withCanManageEachConfigByEmployee(String description, EnumCommonConfig config, CommonConfigProvider provider, boolean expected) {
		assertThat(canManageEachConfigByEmployee(config, provider), is(expected));
	}

	static Stream<Arguments> exceptUndefinedProvider() {
		return Stream.of(
				arguments("USE:ユーザー個別設定【不可】か", USE, null, false),
				arguments("UNUSE:ユーザー個別設定【不可】か",UNUSE, null, false),
				arguments("DEPEND_ON_EMPLOYEE:ユーザー個別設定【可能】か",DEPEND_ON_EMPLOYEE, null, true)
		);
	}

	static Stream<Arguments> undefinedProvider() {
		return Stream.of(
				arguments("UNDEFINED×デフォルトUSE:ユーザー個別設定【不可】か",UNDEFINED, getMockedProvider(USE), false),
				arguments("UNDEFINED×デフォルトUNUSE:ユーザー個別設定【不可】か",UNDEFINED, getMockedProvider(UNUSE), false),
				arguments("UNDEFINED×デフォルトDEPEND_ON_EMPLOYEE:ユーザー個別設定【可能】か",UNDEFINED, getMockedProvider(DEPEND_ON_EMPLOYEE), true),
				arguments("UNDEFINED×デフォルト未設定:ユーザー個別設定【不可】か(Exceptionが吐かれないかも確認)",UNDEFINED, getMockedProvider(UNDEFINED), false) // 異常系
		);
	}

実行結果
image.png

9. staticメソッドをMockしたい⇒【Mockito.mockStatic】

Mockito.mockStaticというメソッドを使ってstaticメソッドを持つクラスをMockすることができます。
注意点は必ずMockしたクラスを必ず解放することです。

書き方や注意点はどは下記を参照してください。

その他

Tips: 実行後のカバレッジのハイライトを消したい

テスト実行すると、ソースのカバレッジ状況が緑/黄/赤でハイライトされます。テストのカバー率がわかるのは便利ですが、コーディングするときに邪魔になる・・・そんなときは、以下の方法でクリアできます。
Window > Show View > Coverage
タブ右側にあるRemove All Sessionsアイコンをクリック
image.png

おわりに:なぜユニットテストを書くべきか、現時点での個人的所感

特に業務上ユニットテストを書くメリットとしては以下が挙げられるかなと。

  • 後からソースコードを見た人がどんなパターンを想定して実装しているかの判断材料になる
  • 文字ベースで書いていた単体テストをコードに置き換えできる
    • たとえばExcelにパターンをしたためるより、そのパターンをコーディングの方が圧倒的に楽しくないですか?
  • 単純なパターンテストをシステムにお願いできる
    • 設定画面でこのパラメータ変更して、実行画面で実行して・・の手間が大幅削減
  • テスト再利用可能
    • 1度作れば何度でも、即再実行できる(もちろん機能追加などによるメンテナンスは必要ですが)
    • ビルドジョブに組み込むことでビルド時に一緒に実行できる
  • リファクタリングするときの安心材料になる

最初のキャッチアップにこそ数時間かかりましたが、書き始めるとソースコードを書くのと同様で楽しいです。
またビルドジョブでユニットテストが失敗して余計な実装を入れていたことに気づくなど、テストを書くことによる心理的安全性を肌で感じました・・・

ソースコードを書くのと同じでテストコードを書くのも経験を積み重ねることでブラッシュアップされると感じるので、テストコードを1度書いて終わりではなく、定期的に振り返ってリファクタリングして、というのをやっていきたいです。

参考URL

  1. 実は社内で同僚がTDDのキャッチアップのために紹介していたのはこちらの動画でした・・・が、どうやら私は途中で関連リンクなどから1つ目の動画を開き、それを先に視聴していたようです。2つとも見てみた結果、どちらから見てもよかったなという印象です。

  2. Quick JUnitプラグインが入っていない場合はHelp > Eclipse Marketplace...から検索し、インストールしてください。

  3. JUnit5ではテストクラスやテストメソッドをパッケージプライベートで書くことができます。JUnit4ではpublicである必要があったようです。

  4. 業務上利用できるのがHamcrestだったため、AssertJなどは未検証です。比較表はこちらなど参考になるかと。

  5. こちらの記事によると、SonarQubeなどの静的解析ツールで、誤って(テストコード以外の)他のクラスのコードからアクセスされていないかをチェックできるようです。

165
160
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
165
160

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?