Wicket + Guice including unittests
Last week I spent way too much time integrating Apache Wicket and Google Guice. Yikes! The most difficult part for me was getting the initialization to happen in the right order. A big Thank You to Dan Retzlaff on the Wicket list for helping work through these details.
The details below were applied to a Wicket quickstart project for Wicket 6.0.0.
Design Decisions
It was important to me to keep the application tier separate from web tier. I actually maintain each in a separate repository. I have several motivations for this, such as:
- Clean separation of concerns. In other words, prevent logic from ending up in my Wicket pages
- Independent revisions and release cycles between web tier and application tier
- Easier to divide work between scrum teams
I include the application tier into the Wicket front end as a jar. By the way, this also makes it easy to include my application tier into a Jersey REST interface and other legacy servlets.
It was also important to maintain a mock package for fast unittests. Since the data providers are managed in the application tier, the mock package lives there, further enforcing the separation of concerns.
Implementation Approach
After some experimentation I decided to use the GuiceServlet approach. I started by adding the following maven dependencies to my pom.xml for the Wicket quickstart.
<dependency> <groupId>com.google.inject</groupId> <artifactId>guice</artifactId> <version>3.0</version> </dependency> <dependency> <groupId>com.google.inject.extensions</groupId> <artifactId>guice-servlet</artifactId> <version>3.0</version> <type>jar</type> </dependency> <dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket-guice</artifactId> <version>6.7.0</version> <type>jar</type> </dependency> |
My web.xml defines only the GuiceFilter and a listener to initialize the injector when the servlet context is created.
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>guicewickettest</display-name> <listener> <listener-class>com.danielwatrous.myapp.web.MyGuiceServletConfig</listener-class> </listener> <filter> <filter-name>guiceFilter</filter-name> <filter-class>com.google.inject.servlet.GuiceFilter</filter-class> </filter> <filter-mapping> <filter-name>guiceFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> |
As you’ll see, this keeps configuration details in code, rather than XML. The web.xml remains simple. That brings us to the GuiceServletConfig, which is where we initialize the injector.
You may recall that listeners receive event notifications at well defined points in the life cycle of a web application or session. Here’s the MyGuiceServletConfig that’s referenced in web.xml:
package com.danielwatrous.myapp.web; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.servlet.GuiceServletContextListener; import com.danielwatrous.myapp.modules.MongoMyappModule; public class MyGuiceServletConfig extends GuiceServletContextListener { @Override protected Injector getInjector() { return Guice.createInjector(new MyappServletModule(), new MongoMyappModule()); } } |
The GuiceServletContextListener implements ServletContextListener, which ends up executing when the application context is created (or destroyed). This way we have the injector available before executing any application code.
Another thing you may notice is that I create the injector with two modules, not just one. The first module is the ServletModule, and I’ll show that to you in a minute. The next module is the application tier module that has been included as a jar in the Wicket application. The single injector available throughout the Wicket application will be able to inject servlet/Wicket related components in addition to application tier components.
Let’s have a closer look at MyappServletModule:
package com.danielwatrous.myapp.web; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.servlet.ServletModule; import java.util.HashMap; import java.util.Map; import org.apache.wicket.protocol.http.IWebApplicationFactory; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.protocol.http.WicketFilter; public class MyappServletModule extends ServletModule { @Override protected void configureServlets() { filter("/*").through(WicketFilter.class, createWicketFilterInitParams()); bind(WebApplication.class).to(WicketApplication.class); bind(WicketFilter.class).to(CustomWicketFilter.class).in(Scopes.SINGLETON); } @Singleton private static class CustomWicketFilter extends WicketFilter { @Inject private Provider<WebApplication> webApplicationProvider; @Override protected IWebApplicationFactory getApplicationFactory() { return new IWebApplicationFactory() { @Override public WebApplication createApplication(WicketFilter filter) { return webApplicationProvider.get(); } @Override public void destroy(WicketFilter filter) { } }; } } private Map<String, String> createWicketFilterInitParams() { Map<String, String> wicketFilterParams = new HashMap<String, String>(); wicketFilterParams.put(WicketFilter.FILTER_MAPPING_PARAM, "/*"); wicketFilterParams.put("applicationClassName", "com.danielwatrous.myapp.web.WicketApplication"); return wicketFilterParams; } } |
You may notice that I added a mechanism to provide WicketFilter with additional parameters. Next I bind my WebApplication and WicketFilter classes to specific implementations. The CustomWicketFilter overrides the typical behavior of the WicketFilter which usually takes a string reference to the WebApplication class. Instead it now uses the injected WebApplication object.
As you’ll see below, this step of injecting the desired WebApplication is critical to enabling unittests, primarily because it allows us to construct the WebApplication with an injector.
package com.danielwatrous.myapp.web; import com.google.inject.Inject; import com.google.inject.Injector; import org.apache.wicket.guice.GuiceComponentInjector; import org.apache.wicket.protocol.http.WebApplication; public class WicketApplication extends WebApplication { private final Injector injector; @Inject public WicketApplication(Injector injector) { this.injector = injector; } @Override public Class<HomePage> getHomePage() { return HomePage.class; } @Override public void init() { super.init(); getComponentInstantiationListeners().add(new GuiceComponentInjector(this, injector)); } } |
At this point Wicket and Guice are successfully integrated. Let’s have a look at what needs to happen to make the unittests work.
Unittests
The only real change that’s required to make the unittests work is in the setUp function of the unittest. Since the WebApplication above was modified to receive an injector, all we need to do is create an injector and provide it at instantiation.
package com.danielwatrous.myapp; import com.google.inject.Guice; import com.google.inject.Injector; import com.danielwatrous.myapp.modules.TestMyappModule; import com.danielwatrous.myapp.web.HomePage; import com.danielwatrous.myapp.web.WicketApplication; import org.apache.wicket.util.tester.WicketTester; import org.junit.Before; import org.junit.Test; public class TestHomePage { private WicketTester tester; @Before public void setUp() { Injector injector = Guice.createInjector(new TestMyappModule()); tester = new WicketTester(new WicketApplication(injector)); } @Test public void homepageRendersSuccessfully() { //start and render the test page tester.startPage(HomePage.class); //assert rendered page class tester.assertRenderedPage(HomePage.class); } } |
You’ll notice in this case I create an injector that doesn’t include the ServletModule and uses the TestMyappModule. Since the Wicket unittests don’t operate within a full web context I don’t need the ServletModule. Additionally my TestMayappModule makes use of my mock package. This allows the tests to run without access to any external resources. It also keeps the tests very fast!
Wicket Pages
Accessing the injector in your Wicket pages is easy. All you need to do is inject it. Here’s how that looks:
package com.danielwatrous.myapp.web; import com.google.inject.Inject; import com.google.inject.Injector; import com.danielwatrous.myapp.domain.QuickLink; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.request.mapper.parameter.PageParameters; public class HomePage extends WebPage { private static final long serialVersionUID = 1L; @Inject private Injector injector; public HomePage(final PageParameters parameters) { super(parameters); add(new Label("version", getApplication().getFrameworkSettings().getVersion())); // TODO Add your page's components here QuickLink quickLink = injector.getInstance(QuickLink.class); add(new Label("quickLink ", quickLink.buildQuickLink())); } } |
Great Combination
After working through the particulars, this implementation feels clean and flexible. Configuration is in the code and benefits from compile time type checking. Unittests are working and fast with very little modification to the traditional Wicket approach, which keeps the application testable.
Resources:
http://apache-wicket.1842946.n4.nabble.com/Wicket-Guice-unittests-td4652853.html
https://gist.github.com/3880246
[…] Wicket + Guice including unittests […]