はじめに
JUnit5 テストの共通前処理用に @BeforeEach
がありますが、複数のテストメソッドで前処理が異なる場合は @Nested
でインナークラスを分けて、個別に @BeforeEach
できます。しかし、これらの多くのケースでは @ParameterizedTest
を使用することにより、@Nested
と @BeforeEach
無しで、同じメソッドに対して複数の条件・期待値を繰り返し引数として渡せるユニットテストを簡潔に記述し、冗長性を回避することができるようになり、絶大な進歩をもたらしました。ですが、@ParameterizedTest
に @CsvSource
でソース上に指定する条件や期待値が String 配列のため、ダブルクォートとカンマで見た目がとても嫌でした。でも、Java 15 でサポートされたテキストブロックが遂に最新の JUnit 5.10 から使えるようになって、超見やすくなりました!(個人の感想です)
テスト対象クラス
なんでもいいですが、例として Jakarta サーブレット。リクエストパラメーター name を受け取って、リクエスト属性に message をセットしています。name パラメーターが無い場合のデフォルト値は "World" です。
package jp.example;
import static java.util.Objects.*;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException
{
String name = req.getParameter("name");
req.setAttribute("message", requireNonNullElse(name, "World"));
req.getRequestDispatcher("/WEB-INF/view/hello.jsp").forward(req, res);
}
}
テストクラス
-
@CsvSource
の textBlock に改行を含むテキストブロック文字列を指定します。 - テストメソッド引数には、行ごとに delimiter で分割された値が繰り返し渡されます。
- 3 列目の「テスト内容」は、テストメソッドに 3 つ目の引数が無いため無視されます。
- # で始まる行はコメント行で、インデントは閉じ """ 以下でなければなりません。
- モックは Spring Boot でもおなじみ、事実上標準の Mockito つこてます。
package jp.example;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class HelloServletTest {
@ParameterizedTest
@CsvSource(delimiter = '|', textBlock = """
# パラメーター | 期待値 | テスト内容
#----------------------------------------------------
| World | null(パラメーター無し)
'' | '' | 空文字
' ' | ' ' | 空白
' all ' | ' all ' | 両端空白
Test ① | Test ① | マルチバイト
""")
void doGet(String param, String expected) throws IOException, ServletException {
var req = mock(HttpServletRequest.class);
doReturn(mock(RequestDispatcher.class)).when(req).getRequestDispatcher(any());
doReturn(param).when(req).getParameter("name");
new HelloServlet().doGet(req, any());
verify(req).setAttribute("message", expected);
}
}
テスト結果には、ちゃんとテキストブロック行数分の条件と結果が個別に表示されます。メソッドに 3 つ目の引数をダミーで追加すれば、結果にも表示されて分かりやすくなるかもしれません。
IntelliJ や Eclipse などの IDE では、テスト結果がさらに見やすくツリー表示され、行ごとに再実行できたりします。
HTML レポート | IntelliJ | Eclipse | NetBeans | VSCode | |
---|---|---|---|---|---|
行ごとの結果表示 | 〇 | 〇 | 〇 | 〇 | 〇 |
行ごとの条件表示 | 〇 | 〇 | 〇 | × | 〇 |
├ パラメーター名表示 | 〇*1 | 〇*1 | 〇*2 | × | 〇*3 |
└ 条件の空白表示 | 〇*1 | 〇*4 | 〇 | × | 〇*1 |
行ごとの再実行 | - | 〇*4 | 〇 | × | 〇 |
- (*1) build.gradle
compileTestJava.options.compilerArgs << '-parameters'
- (*2) Eclipse > 設定 > Java > コンパイラー > メソッド・パラメーターの情報を保管: ON
- (*3) {プロジェクト}/.settings/org.eclipse.jdt.core.prefs
org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
- (*4) IntelliJ > 設定 > ビルド、実行、デプロイ > ビルドツール > Gradle > テストの実行に使用: 「Gradle (デフォルト)」を「IntelliJ IDEA」に変更
ビルド設定
build.gradle に下記をコピペし、上記の Java ソースを適切な場所に配置してください。
plugins {
id 'war'
}
java.sourceCompatibility = 21
repositories.mavenCentral()
dependencies {
compileOnly 'org.apache.tomcat:tomcat-servlet-api:10.1.+'
testImplementation 'org.mockito:mockito-junit-jupiter:5.+'
testing.suites.test.useJUnitJupiter('5.+')
configurations { testImplementation.extendsFrom compileOnly }
}
おわりに
@ParameterizedTest
と @CsvSource
のテキストブロックで、テストコードの保守性が向上し、テストケースの追加や変更が容易になりました。ぜひ、試してみてください。