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:

  1. Compile-time type-safety
  2. By not hardcoding the runtime component paths into the tests, you can make your tests truly modular!
  3. You save 60% of your time (the time you would have wasted banging your head to the wall of wrong paths and components)
  4. The code is cleaner and nicer, easier to maintain
  5. The test-code is easier to read and makes much more sense as a spec
  • No labels