第十三戒: 試練と証明 〜 WicketTester は神の試練なり

「テストなき信仰は盲信なり。WicketTester の試練を受けよ。さすれば汝のコードは神に認められるであろう」 ーー Wicket 聖典 第十三戒より

Wicket には WicketTester という強力なテストフレームワークが組み込まれています。 ブラウザを起動せずに、Java コードだけでページのレンダリング、フォーム送信、 AJAX 操作をテストできます。

Wicket のテスト

Wicket のコンポーネント指向アーキテクチャにより、 UIコンポーネントのユニットテストが非常に書きやすくなっています。 WicketTester は以下のことが可能です:

テスト環境のセットアップ

Wicket 10 の変更点: WicketTester は Wicket 10 で wicket-core から独立した wicket-tester モジュールに分離されました。 テスト依存に追加する必要があります。

pom.xml(テスト依存の追加)
<dependencies>
    <!-- Wicket テスト -->
    <dependency>
        <groupId>org.apache.wicket</groupId>
        <artifactId>wicket-tester</artifactId>
        <version>10.8.0</version>
        <scope>test</scope>
    </dependency>

    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

WicketTester の基本

WicketTesterWebApplication のインスタンスを受け取って初期化します。 テストクラスの典型的な構造は以下の通りです。

import org.apache.wicket.util.tester.WicketTester;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class HomePageTest {

    private WicketTester tester;

    @BeforeEach
    void setUp() {
        tester = new WicketTester(new WicketApplication());
    }

    @Test
    void homePageRendersSuccessfully() {
        // ホームページをレンダリング
        tester.startPage(HomePage.class);

        // HTTP 200 が返ることを確認
        tester.assertRenderedPage(HomePage.class);

        // エラーメッセージがないことを確認
        tester.assertNoErrorMessage();
    }
}

ページレンダリングテスト

@Test
void testPageRendering() {
    tester.startPage(HomePage.class);

    // 正しいページがレンダリングされたか
    tester.assertRenderedPage(HomePage.class);

    // 特定のコンポーネントが存在するか
    tester.assertComponent("message", Label.class);

    // コンポーネントの表示内容を検証
    tester.assertLabel("message", "Hello, Wicket!");

    // コンポーネントが表示されているか
    tester.assertVisible("message");

    // コンポーネントが非表示か
    tester.assertInvisible("hiddenPanel");
}

@Test
void testPageWithParameters() {
    // PageParameters 付きでページをレンダリング
    PageParameters params = new PageParameters();
    params.add("id", 42);

    tester.startPage(UserDetailPage.class, params);
    tester.assertRenderedPage(UserDetailPage.class);
}

コンポーネントのテスト

コンポーネントのパスは、ページルートからの : 区切りのパスで指定します。

@Test
void testComponents() {
    tester.startPage(UserPage.class);

    // 直接の子コンポーネント
    tester.assertComponent("userPanel", UserInfoPanel.class);

    // ネストしたコンポーネント(パスは : 区切り)
    tester.assertComponent("userPanel:name", Label.class);

    // フォーム内のコンポーネント
    tester.assertComponent("form:username", TextField.class);

    // コンポーネントのモデル値を検証
    tester.assertModelValue("userPanel:name", "田中太郎");

    // HTML 出力に特定の文字列が含まれるか
    tester.assertContains("田中太郎");
}

フォーム送信テスト

FormTester を使うと、フォームの入力と送信をシミュレートできます。

@Test
void testFormSubmit() {
    tester.startPage(LoginPage.class);

    // FormTester を取得(パスはフォームの wicket:id)
    FormTester formTester = tester.newFormTester("loginForm");

    // フォームに値を入力
    formTester.setValue("username", "admin");
    formTester.setValue("password", "secret123");

    // フォームを送信
    formTester.submit();

    // 送信後のページを検証
    tester.assertRenderedPage(DashboardPage.class);
    tester.assertNoErrorMessage();
}

@Test
void testFormValidation() {
    tester.startPage(LoginPage.class);

    FormTester formTester = tester.newFormTester("loginForm");

    // 必須フィールドを空のまま送信
    formTester.setValue("username", "");
    formTester.submit();

    // 同じページに留まる(バリデーションエラー)
    tester.assertRenderedPage(LoginPage.class);

    // エラーメッセージが表示されている
    tester.assertErrorMessages("'username' は必須です。");
}

フォームの各種入力

FormTester formTester = tester.newFormTester("form");

// テキストフィールド
formTester.setValue("name", "田中太郎");

// チェックボックス
formTester.setValue("agree", true);

// ドロップダウン(インデックスで選択)
formTester.select("city", 2); // 3番目の選択肢

// ラジオボタン(インデックスで選択)
formTester.select("gender", 0);

// 特定のボタンで送信
formTester.submit("saveButton");
@Test
void testLinkClick() {
    tester.startPage(HomePage.class);

    // リンクをクリック
    tester.clickLink("aboutLink");

    // 遷移先のページを検証
    tester.assertRenderedPage(AboutPage.class);
}

@Test
void testBookmarkableLink() {
    tester.startPage(HomePage.class);

    // BookmarkablePageLink の遷移先を検証
    tester.assertBookmarkablePageLink("userListLink",
        UserListPage.class, new PageParameters());
}

AJAX テスト

AJAX コンポーネントのテストも WicketTester でサポートされています。

@Test
void testAjaxLink() {
    tester.startPage(CounterPage.class);

    // 初期値を確認
    tester.assertLabel("count", "0");

    // AjaxLink をクリック
    tester.clickLink("increment");

    // AJAX 更新後の値を確認
    tester.assertLabel("count", "1");

    // もう一度クリック
    tester.clickLink("increment");
    tester.assertLabel("count", "2");
}

@Test
void testAjaxButton() {
    tester.startPage(SearchPage.class);

    FormTester formTester = tester.newFormTester("searchForm");
    formTester.setValue("query", "Wicket");

    // AjaxButton をクリック(AJAX で送信)
    tester.executeAjaxEvent("searchForm:searchBtn", "click");

    // AJAX 更新後の結果を検証
    tester.assertContains("Wicket");
    tester.assertNoErrorMessage();
}

@Test
void testAjaxBehavior() {
    tester.startPage(MyPage.class);

    // AjaxEventBehavior を発火させる
    tester.executeAjaxEvent("clickableBox", "click");

    // 結果を検証
    tester.assertComponentOnAjaxResponse("feedbackPanel");
}

tester.clickLink() は AJAX リンクと通常リンクの両方に使えます。 Wicket が自動的に判別して適切に処理します。

FeedbackPanel のテスト

@Test
void testFeedbackMessages() {
    tester.startPage(RegistrationPage.class);

    FormTester formTester = tester.newFormTester("regForm");
    formTester.submit();

    // エラーメッセージがあることを確認
    tester.assertErrorMessages(new String[]{
        "'name' は必須です。",
        "'email' は必須です。"
    });

    // 情報メッセージの検証
    // tester.assertInfoMessages("保存しました");

    // エラーメッセージがないことを確認
    // tester.assertNoErrorMessage();

    // 情報メッセージがないことを確認
    // tester.assertNoInfoMessage();
}

テストのベストプラクティス

テスト用の Application を用意する

テスト時には DB 接続やサービスをモック化した Application を使うと便利です。

public class TestApplication extends WicketApplication {
    @Override
    public void init() {
        super.init();
        // テスト用の設定
        // モックサービスの登録など
    }
}

// テストクラスで使用
@BeforeEach
void setUp() {
    tester = new WicketTester(new TestApplication());
}

Panel のユニットテスト

Panel を単独でテストするには startComponentInPage() を使います。

@Test
void testPanel() {
    User user = new User("田中太郎", "[email protected]");

    // Panel を仮のページに配置してテスト
    tester.startComponentInPage(
        new UserInfoPanel("panel", Model.of(user)));

    tester.assertLabel("panel:name", "田中太郎");
    tester.assertLabel("panel:email", "[email protected]");
}

主要なアサーションメソッド一覧

メソッド用途
assertRenderedPage(Class)レンダリングされたページのクラスを検証
assertComponent(path, Class)指定パスのコンポーネントのクラスを検証
assertLabel(path, expected)Label の表示テキストを検証
assertModelValue(path, expected)コンポーネントのモデル値を検証
assertVisible(path)コンポーネントが表示されていることを検証
assertInvisible(path)コンポーネントが非表示であることを検証
assertEnabled(path)コンポーネントが有効であることを検証
assertDisabled(path)コンポーネントが無効であることを検証
assertContains(regexp)HTML出力に正規表現がマッチすることを検証
assertErrorMessages(msgs)エラーメッセージを検証
assertNoErrorMessage()エラーメッセージがないことを検証
assertComponentOnAjaxResponse(path)AJAX レスポンスにコンポーネントが含まれることを検証

WicketTester はサーブレットコンテナなしで動作するため、テストの実行が非常に高速です。 CI/CD パイプラインにも容易に組み込めます。

実務でハマりやすい注意点

  • 問題
    開発環境では出ないのに、本番で期限切れページの問題が多発する。障害報告として後から顕在化しやすい。
    原因
    戻る操作、タブ複製、新規タブなどの実運用シナリオをテスト観点に入れていない。日常操作が未検証になっている。
    対策
    期限切れページを含む操作シナリオを E2E/結合テストに組み込む。再現しやすい手順をテストケースとして固定する。
  • 問題
    Ajax テストは成功するのに、実画面では機能が壊れている。テスト結果と体感品質が一致しない。
    原因
    コンポーネント更新の確認だけで終わり、再描画後の JavaScript 再初期化を検証していない。フロント側の復旧が抜けている。
    対策
    Ajax 応答の検証に加えて、再初期化後の UI 動作も必ず確認する。画面操作ベースのテストを併用する。
  • 問題
    セッションが切れた瞬間、ユーザーがどこにも進めなくなる。更新ボタンを押しても反応しない状態になる。
    原因
    Ajax 失敗時の遷移や再認証導線を事前にテストしていない。タイムアウト時の復帰仕様が未確認のままになっている。
    対策
    セッションタイムアウトを強制発生させるテストを作成し、ログイン画面へ復帰できることを確認する。復帰後の再操作も検証する。