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
// 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.
-
Locate by what it is
A
Locatorstarts 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) -
Resolve exactly one
On the page the tester rendered last. If nothing or several components match, you get an
AssertionErrorthat says what was searched, what was found, and which components almost matched.Label label = kit.one(customer); List<Label> all = customer.all(page); -
Act through WicketTester
clickisclickLink,ajaxisexecuteAjaxEvent,formis aFormTester,assertLabelisassertLabel. 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.
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
AjaxButtonuses Ajax, and the form helper’sajaxsends 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.
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.
ordersPage.rowOf(globex).ship();
ordersPage.rowOf(globex).assertStatus("shipped");
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.
<dependency>
<groupId>dev.jbaby</groupId>
<artifactId>wicket-testkit</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>
testImplementation("dev.jbaby:wicket-testkit:0.1.0")
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(),insideorinRowOf; useassertCountfor “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.
inRowOfcoversListView,RefreshingViewandDataView. 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.