If you use Test Driven Development in Wicket, the first approach is often to use component's path names (e.g., page:panel:form:component) to test them. This is bearable at first, but as your pages get more and more complicated and have deeper hierarhices, you will find yourself spending a whole lot of time debugging typos and misconceptions (in my opinnion: you would waste 60% of your time doing this). The solution is: make your component hierarcy type-safe so that you can GET all of your components when testing:
public class TestHomePage extends TestCase { private WicketTester tester; @Override public void setUp() { tester = new WicketTester(new WicketApplication()); } /** * */ public void testBookList() { // start and render the test page tester.startPage(HomePage.class); // assert rendered page class tester.assertRenderedPage(HomePage.class); // assert rendered label component tester.assertLabel("message", "If you see this message wicket is properly configured and running"); // assert rendered page class HomePage homePage = (HomePage) tester.getLastRenderedPage(); BookForm bookForm = homePage.getForm(); BookListView bookListView = bookForm.getBookListView(); List<BookListItem> bookItems = toLinkedList(bookListView.iterator()); { // Test values of 1st book BookListItem bookListItem = bookItems.get(0); // Note: no hassle with run-time component path id's assertEquals("first", bookListItem.getNameField().getValue()); assertEquals("", bookListItem.getAuthorField().getValue()); assertEquals("-1", bookListItem.getTypeChoice().getValue()); } { // Test values of 2nd book BookListItem bookListItem = bookItems.get(1); // Note: no hassle with run-time component path id's assertEquals("second", bookListItem.getNameField().getValue()); assertEquals("", bookListItem.getAuthorField().getValue()); assertEquals("-1", bookListItem.getTypeChoice().getValue()); } { // Test values of 3rd book BookListItem bookListItem = bookItems.get(2); // Note: no hassle with run-time component path id's assertEquals("third", bookListItem.getNameField().getValue()); assertEquals("", bookListItem.getAuthorField().getValue()); assertEquals("-1", bookListItem.getTypeChoice().getValue()); } { // Set new values FormTester formTester = tester.newFormTester(bookForm .getPageRelativePath()); // formTester.setValue(bookItems.get(1).getAuthorField(), "Hemingway"); // TODO New feature request tester.getServletRequest().setParameter( bookItems.get(1).getAuthorField().getInputName(), "Hemingway"); // Submit changes // formTester.submit(bookForm.getSubmitButton()); // TODO New feature request formTester.submit(); } tester.assertRenderedPage(HomePage.class); // Verify the submitted value { // Test values of 2nd book BookListItem bookListItem = bookItems.get(1); // Note: no hassle with run-time component path id's assertEquals("second", bookListItem.getNameField().getValue()); assertEquals("Hemingway", bookListItem.getAuthorField().getValue()); assertEquals("-1", bookListItem.getTypeChoice().getValue()); } } /** * @param <T> * @param iterator * @return List<T> */ public static <T> List<T> toLinkedList(Iterator<T> iterator) { List<T> linkedList = new LinkedList<T>(); for (; iterator.hasNext();) { linkedList.add(iterator.next()); } return linkedList; } }
You can see the benefits for yourself compared to using string-paths. In order to accomplish the above, you need a testable listview component. You might need to tune also other Wicket components, but here is an example of making the ListView type-safe:
public abstract class TestableListView<ListItemType extends ListItem<ItemType>, ItemType> extends ListView<ItemType> { public TestableListView(String id, IModel<List<ItemType>> model) { super(id, model); } public TestableListView(String id, List<ItemType> list) { super(id, list); } public TestableListView(String id) { super(id); } @Override protected final void populateItem(ListItem<ItemType> item) { // do nothing here } @Override protected abstract ListItemType newItem(int index); @Override public Iterator<ListItemType> iterator() { return (Iterator<ListItemType>) super.iterator(); } }
With this TestableListView coding the Book example is easy as follows:
public class HomePage extends WebPage { private static final long serialVersionUID = 1L; private final BookForm form; /** * */ @SuppressWarnings("serial") public HomePage() { add(new Label("message", "If you see this message wicket is properly configured and running")); add(form = new BookForm("form")); } /** * @return the form */ public BookForm getForm() { return form; } } class BookForm extends Form<Void> { private final BookListView bookListView; private final Button submitButton; public BookForm(String id) { super(id); List<Book> books = Arrays.asList(new Book("first"), new Book("second"), new Book("third")); add(bookListView = new BookListView("book_list", books)); add(submitButton = new Button("submit")); } /** * @return the bookListView */ public BookListView getBookListView() { return bookListView; } /** * @return the submitButton */ public Button getSubmitButton() { return submitButton; } } class Book implements Serializable { /** Field name (reflection property expression) */ public static final String NAME = "name"; /** Field name (reflection property expression) */ public static final String AUTHOR = "author"; /** Field name (reflection property expression) */ public static final String TYPE = "type"; private String name; private String author; private Type type; /** @param name */ public Book(String name) { this.name = name; } public enum Type { OLD, NEW; } } class BookListView extends TestableListView<BookListItem, Book> { public BookListView(String id, List<Book> list) { super(id, list); } @Override protected BookListItem newItem(int index) { return new BookListItem(index, getListItemModel(getModel(), index)); } } class BookListItem extends ListItem<Book> { private final TextField<String> nameField; private final TextField<String> authorField; private final DropDownChoice<Type> typeChoice; public BookListItem(int index, IModel<Book> model) { super(index, model); Book book = model.getObject(); add(nameField = new TextField<String>("name", new PropertyModel<String>(book, Book.NAME))); add(authorField = new TextField<String>("author", new PropertyModel<String>(book, Book.AUTHOR))); add(typeChoice = new DropDownChoice<Type>("type", new PropertyModel<Type>(book, Book.TYPE), Arrays.asList(Type.values()))); } /** * @return the nameField */ public TextField<String> getNameField() { return nameField; } /** * @return the authorField */ public TextField<String> getAuthorField() { return authorField; } /** * @return the typeChoice */ public DropDownChoice<Type> getTypeChoice() { return typeChoice; } }
The underlying markup remains conventional:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.sourceforge.net"> <head> <title>Wicket Quickstart Archetype Homepage</title> </head> <body> <strong>Wicket Quickstart Archetype Homepage</strong> <br/><br/> <span wicket:id="message">message will be here</span> <form wicket:id="form"> <table border="1"> <tr wicket:id="book_list"> <th>Name:</th><td><input type="text" wicket:id="name"/></td> <th>Author:</th><td><input type="text" wicket:id="author"/></td> <th>Type:</th><td><select wicket:id="type"></select></td> </tr> </table> <input type="submit" wicket:id="submit" value="Submit"/> </form> </body> </html>
The benefits of the introduced approach are:
- Compile-time type-safety
- By not hardcoding the runtime component paths into the tests, you can make your tests truly modular!
- You save 60% of your time (the time you would have wasted banging your head to the wall of wrong paths and components)
- The code is cleaner and nicer, easier to maintain
- The test-code is easier to read and makes much more sense as a spec