wicket-testkit

Test kit for Apache Wicket 10

Find components by what they arenot where they sit.

WicketTester finds components by paths like orders:1:customer. Wrap a container around them or sort a list, and the test breaks, or checks the wrong row. wicket-testkit locates components by type, wicket:id, model object and row, then acts and asserts through the WicketTester you already have.

  • Wicket 10
  • Java 17+
  • test scope
  • no extra dependencies
  • Apache 2.0
OrdersPageTest.java
// before: paths that break on the next refactoring
tester.assertLabel("orders:1:customer", "Globex");
tester.clickLink("orders:1:ship");

// after: say what you mean
var kit = WicketTestkit.of(tester);

kit.assertLabel(locate(Label.class).withId("customer")
    .inRowOf(globex), "Globex");
kit.click(locate(AjaxLink.class).withId("ship").inRowOf(globex));

Why

Paths break. Locators don’t.

Here is a page with three orders, and two checks of the same label. Change the page the way real pages change, and watch which check still finds Globex.

With a path

tester.assertLabel("orders:1:customer", "Globex");

Passes: row 1 shows Globex, for now.

With a locator

kit.assertLabel(locate(Label.class).withId("customer")
    .inRowOf(globex), "Globex");

Passes: finds Globex’s customer label at orders:1:customer.

The page’s component tree

    How it works

    Locate, resolve, act.

    A thin layer over WicketTester. Every method finds exactly one component and hands it to the WicketTester method you know, so Wicket’s behaviour stays exactly as it is.

    1. Locate by what it is

      A Locator starts with a type and narrows down: id, model object, visible, enabled, inside another component, in the row of an element, or any predicate. It is immutable, so page objects can keep it and refine it.

      locate(Label.class)
          .withId("customer")
          .inRowOf(order)
    2. Resolve exactly one

      On the page the tester rendered last. If nothing or several components match, you get an AssertionError that says what was searched, what was found, and which components almost matched.

      Label label = kit.one(customer);
      List<Label> all = customer.all(page);
    3. Act through WicketTester

      click is clickLink, ajax is executeAjaxEvent, form is a FormTester, assertLabel is assertLabel. Nothing is re-implemented, and your tester stays usable as before.

      kit.click(ship);
      kit.assertLabel(status, "shipped");
      kit.assertOnAjaxResponse(status);

    Failure messages

    When a test fails, it says why.

    Failure messages are part of the design, specified and tested like everything else.

    • Near misses. Nothing matched? You see the components that met every criterion but one, and what differs. A component of another type, or a hidden one, comes first.
    • Several matches. Every path, so you know which refinement to add.
    • Context for WicketTester’s failures. The locator and the component’s path go in front. Expected and actual are kept, so your IDE still shows its diff.
    Expected exactly one match for a Label with id "customr" in the row of Order[id=42, customer=Globex] in OrdersPage, but found none.
    Near misses (all criteria but one):
      - orders:1:customer (has id "customer")
      - orders:1:status (has id "status")
    A typo in the id: the near misses show the ids that exist in that row.
    Expected exactly one match for an AjaxLink with id "ship" in OrdersPage, but found 3:
      - orders:0:ship
      - orders:1:ship
      - orders:2:ship
    Every row has a ship link. Add inRowOf(order).
    a Label with id "customer" in the row of Order[id=42, customer=Globex] (at orders:1:customer): expected: <ACME> but was: <Globex>
    WicketTester’s own failure, with the locator and the path in front.

    Forms

    Fill in forms by field, not by form-relative path.

    kit.form(…) wraps a FormTester and finds fields and buttons inside that form, however deep they are nested.

    • Ajax included. Submitting through an AjaxButton uses Ajax, and the form helper’s ajax sends the values set so far, so an updating behavior sees the new input.
    • No silent losses. A disabled or hidden field fails instead of dropping your value.
    • Step by step. Within one request, kit.form(…) returns the same helper, so a page object can fill a form across several methods.
    CheckoutTest.java
    kit.form(locate(Form.class).withId("order"))
        .set(locate(TextField.class).withId("customer"), "Globex")
        .select(locate(DropDownChoice.class).withId("country"), 1)
        .submit(locate(AjaxButton.class).withId("save"));
    
    kit.assertFeedback(locate(FeedbackPanel.class).visible(), "Order saved.");
    
    // an AjaxFormComponentUpdatingBehavior, with the new value
    kit.form(locate(Form.class).withId("order"))
        .select(locate(DropDownChoice.class).withId("country"), 2)
        .ajax(locate(DropDownChoice.class).withId("country"), "change");

    Page objects

    Tests in the words of your domain.

    Locators are values. Keep them in a page object, refine them per row, and your tests read like the requirement they prove.

    ShippingTest.java
    ordersPage.rowOf(globex).ship();
    ordersPage.rowOf(globex).assertStatus("shipped");
    OrdersPageObject.java
    class OrdersPageObject {
    
        private final WicketTestkit kit;
    
        OrdersPageObject(WicketTestkit kit) {
            this.kit = kit;
        }
    
        Row rowOf(Order order) {
            return new Row(order);
        }
    
        class Row {
    
            private final Order order;
    
            Row(Order order) {
                this.order = order;
            }
    
            void ship() {
                kit.click(locate(AjaxLink.class).withId("ship").inRowOf(order));
            }
    
            void assertStatus(String status) {
                kit.assertLabel(locate(Label.class).withId("status").inRowOf(order), status);
            }
        }
    }

    Install

    One test dependency.

    wicket-testkit uses the wicket-core and wicket-tester your project already has, so your Wicket version wins. Wicket 10, Java 17 or newer.

    Latest release: 0.1.0, on Maven Central.

    pom.xml
    <dependency>
        <groupId>dev.jbaby</groupId>
        <artifactId>wicket-testkit</artifactId>
        <version>0.1.0</version>
        <scope>test</scope>
    </dependency>
    Your first test
    import static dev.jbaby.wicket.testkit.locator.Locator.locate;
    
    import dev.jbaby.wicket.testkit.WicketTestkit;
    
    class OrdersPageTest {
    
        WicketTester tester = new WicketTester(new MyApplication());
        WicketTestkit kit = WicketTestkit.of(tester);
    
        @Test
        void showsTheTitle() {
            tester.startPage(OrdersPage.class);
    
            kit.assertLabel(locate(Label.class).withId("title"), "Orders");
        }
    }

    Before you rely on it

    What to know.

    • Exactly one, always. Actions and assertions need exactly one match. Narrow with visible(), inside or inRowOf; use assertCount for “none”.
    • The last rendered page. Locators are resolved against the page the tester rendered last. To search a part of it, use inside.
    • Rows are list items. inRowOf covers ListView, RefreshingView and DataView. For entities that only equal themselves, match the row with a predicate.
    • Test scope only. Production code that walks the component tree should use Wicket’s own visitChildren.
    • A component under test. After startComponentInPage, everything inside the component can be located, but not the component itself.
    • Wicket 10. Wicket 9 and older are not supported; Wicket 11 will follow.

    All the details, and the reasons, are in the user guide.