Excerpt | ||
---|---|---|
| ||
How to achieve cleaner code and compile-time type-safety for your tests, probably saving 60% of your time |
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 good 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:
Code Block |
---|
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:
Code Block |
---|
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:
Code Block |
---|
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:
Code Block |
---|
<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