Abbreviated component paths in wicket tester

Wicket tester combined with Mockito is fantastic for unit testing your web application. However, developing the tests is harder than it should be because you have to refer to components by their path rather than their id.

For example, to clock the OK button on a page, I need something like:

tester.executeAjaxEvent(“border:form:pagetitlecontainer:ok”. “onclick”);

i.e. instead of “ok”, I must refer to the button by its full path “border:form:pagetitlecontainer:ok”. This seems tedious – if I know that I only have one component on the page with the id “ok”, why can’t I just use “ok”. Wicket tester should notice that the component is not available and traverse the component hierarchy for it. If its ambiguous then it can complain, but if not, then it should be OK.

To get around this limitation, we override the getComponentFromLastRenderedPage method of the WicketTester class as follows:

@Override
public Component getComponentFromLastRenderedPage(final String path) {
	// first check if we can find the component with the specified path - if not, check if its an abbreviated path
	String fullPath = lookupPath(getLastRenderedPage(), path);
	if (fullPath == null)
		return null;

	return super.getComponentFromLastRenderedPage(fullPath);
}

public String lookupPath(final MarkupContainer markupContainer, final String path) {
	// try to look it up directly
	if (markupContainer.get(path) != null)
		return path;

	// if that fails, traverse the component hierarchy looking for it
	final List<component> candidates = new ArrayList<component>();
	markupContainer.visitChildren(new IVisitor<component>() {
		private static final long serialVersionUID = 1L;

		Set<component> visited = new HashSet<component>();

		@Override
		public Object component(Component c) {
			if (!visited.contains(c)) {
				visited.add(c);

				if (c.getId().equals(path))
					candidates.add(c);
			}
			return IVisitor.CONTINUE_TRAVERSAL;
		}
	});
	// if its unambiguous, then return the full path
	if (candidates.isEmpty()) {
		fail("path: '" + path + "' not found for " +
				Classes.simpleName(markupContainer.getClass()));
		return null;
	} else if (candidates.size() == 1) {
		String pathToContainer = markupContainer.getPath();
		String pathToComponent = candidates.get(0).getPath();
		return pathToComponent.replaceFirst(pathToContainer + ":", "");
	} else {
		String message = "path: '" + path + "' is ambiguous for " + Classes.simpleName(markupContainer.getClass()) + ". Possible candidates are: ";
		for (Component c : candidates) {
			message += "[" + c.getPath() + "]";
		}
		fail(message);
		return null;
	}
}

The Palchinsky Principles

My holiday reading this year was “Adapt” by Tim Harford. One of the most interesting parts was about Peter Palchinsky, a great russian engineer who was an advisor to both the Tsar and to Stalin. Although his uncompromising honesty got him exiled to Siberia by the Tsar, pardoned again and finally murdered by Stalin’s secret police, he nevertheless had time to formulate 3 principles for innovation:

  1. Try lots of things, expecting many to fail.
  2. Make sure the failures are survivable.
  3. Learn from the failures.

As Tim Harford points out, this is roughly how evolution works in nature and it seems applicable to software development. Since customers don’t generally appreciate failures on their projects, this trial and error cycle needs to be carried out outside of production projects – in the “20%” projects we do on our own time.