You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 4 Next »

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
  • No labels