Automated Integration Testing with HttpUnit and ServletUnit
One frequent hole in Test Driven Development (or any other comprehensive attempt to test) in Java web applications is the presentation layer. Especially when using an MVC pattern to separate the work from the display, the trend is to do that testing more or less "by hand." After the rest has been written and tested, the application is packaged and deployed, and testing is done by skipping though websites, filling boxes and clicking buttons. There are a number of scripting and other automated test tools that will "drive" a browser or even emulate one, reducing or removing the human element from that testing, but that package and deploy step still has to be done.
It's well and widely accepted to use unit testing to ensure the smaller bits and pieces of software do what is expected. Done right, every bit of the model and controller of an MVC can be tested and validated before being deemed to be a success enough to release. Breaking that package and deploy to test cycle brings a lot of bug fixing back into the development cycle, hopefully before involving the test cycle with other people.
No Container, Please
There are plenty of Servlet engines and application containers that are written in Java, just like the Servlets and applications they contain. It always struck me as odd that one couldn't just start a test process at the point where the engine gets a request, accepting the resulting output as returned by the engine. It seemed to be a fairly straight forward concept to invoke a servlet engine directly. Using Spring, it should be possible to configure a DispatcherServlet with enough of the information from the web.xml and other configuration XML files to allow it to accept a request and return the rendered response.
I also figured I couldn't be the first person to want such a thing. While looking to see what others had thought of this kind testing, I stumbled upon HttpUnit and ServletUnit at SourceForge one day. HttpUnit largely allows writing tests with its mock client, letting the tests act as a browser. A step away from some of the other automated scripting methods, it puts all of that scripting within the construct of the other unit tests, but still required the application to be packaged and deployed. It does give access to all kinds of methods of testing the responses, so that one doesn't have to go about parsing the resulting HTML or other output, which is nice.
Within the HttpUnit product, however, is the ServletUnit suite of tools. ServletUnit does the work of running the Servlet as if in a container, accepting a request, and getting the response. It does this as other kinds of unit tests, like JUnit, allowing the developer to formulate an end-to-end test of a controller. It does this in a way that integrates the servlet engine into the equation, allowing simultaneous access to both the client and server for the life of the test, allowing validation to do more than evaluate the response, but also to investigate the server to validate the state of the models and controllers involved. Additionally, there's no server actually running, so the state of the server is fresh with each test.
Same Test Suite
While this can run along side unit tests, it is is not a unit test, but truly an integration test. The test will run through all of the parts that a Servlet engine would need to, not just the little bits. This requires some additional consideration as tests are authored to prepare a comprehensive set of data and configurations for the test to work on. Since the test is running as would any other unit test, and not a Servlet or application container, all of the testing tools are available, such as mock objects. And since the tests are kicked off with the unit test runner, other test-related tools such as code coverage can also be used.
In the end, what we've been given is a way to add tests to our JSPs and Servlets from request to response.
Download and Install
Downloading is easy, of course. Visit the http://httpunit.sourceforge.net/ web page and bonk the download link. The SourceForge downloader will download the current httpunit-version.zip file.
Installing is fairly easy, with the complications of the servlet engine made harder or easier based on some of your intent. The easiest solution is to take the httpunit.jar from the zip file's "lib" folder and add all of the other files from the "jars" folder to the project's classpath. This is unfortunately often overkill, especially when working in Eclipse or another IDE where there may be a Servlet engine already in the classpath. Let's look at a leaner installation.
Example Project: Foo
Let's cut the chatter and demonstrate this with a trivial Servlet application. While it might behoove me to start with a simple JSP or straight-up Servlet example, I'm going to hurdle straight into a Spring framework application.
What we're going to test is the wiring of a Servlet, and its execution. A simple one, to be sure, but the critical bits will be there. I'll make a simple two-JSP site that contains an authentication page and a page to display when authenticated. A single bean and a single annotated Controller will be used, forcing authentication if a session token isn't found, or allowing the second page to be displayed if it's already filled. We'll write a couple trivial JUnit tests, and a couple trivial ServletUnit tests to show the difference between the two.
Eclipse Projects
I'm a fan of separating base code from test code, not only by having separate source folders, but also in separate classpaths. Separating the classpaths makes sure that base code doesn't accidentally end up in the test source folder, and it ensures that the base code doesn't accidentally use test classes. Using Ant allows separating the classpaths for the build process and the test execution by having separate classpaths entries for the different targets. This separation is also easy within the same project in NetBeans, as the project configurations separate the runtime and test classpaths by using Ant underneath. In Eclipse this can be easily accomplished by having separate projects for the base code and tests.
Unfortunately, this adds a little difficulty as projects then have project dependencies, and sometimes extra dependencies. But once the projects are configured, it's just a matter of ensuring both are present. Additionally, once you get used to dual projects like this, it becomes second nature.
I'll model this with two Eclipse projects; Foo and FooTest. Foo is built as a Dynamic Web project, while FooTest is a simple Java project. Foo will be configured using a Servlet engine, like Tomcat; the rest of the example assumes Tomcat v6, but it should be a small matter to use your favorite or necessary container. This requires an installation of Tomcat somewhere, with the appropriate bits configured in the project; in Eclipse simply visit the Window/Preferences and find the Server/Runtime Environments and add yours (the new project wizard will do this for you if one isn't already configured). Then make sure the projects use this runtime.
Foo
The default structure of Foo/src containing the Java code and Foo/WebContent containing the, well, web content. In the WebContent folder is the protected WEB-INF folder; this will contain the web.xml, the magic lib folder, and a jsp folder for holding the JSPs out of sight from the web server. Also in the WebContent folder will be any other static content that the applciation may deliver, such as images. As needed, JARs are simply added to the WebContent/lib folder, such as the Spring framework, Hibernate, and whatever else the project may require. In addition to the Eclipse default of having the Foo/src folder as a source folder, we need to add the Foo/WebContent folder as a source folder to allow our test project to find its contents in the classpath.
FooTest
The FooTest will contain simply FooTest/src, containing the JUnit test code, and a FooTest/lib folder into which we'll put our test-specific libraries, including the HttpUnit JARs. Since the intent of this project is to test the Foo project, it needs to have the Foo project added to its path, for access to the base code and JARs. It also needs to have the Servlet container (library) added.
Tomcat Server
Since we're talking about using the ServletUnit suite, and the example is using Tomcat, we may need to have more JARs from the Tomcat server path, too. The Tomcat library contains the libraries to which a Servlet running in the container would have access, but ServletUnit needs to have the libraries that the server needs to run, too.
Tomcat Tweak
For my installation of Tomcat 6, I needed the TOMCAT_HOME/bin/tomcat-juli.jar to be added; your version may have different needs--see what breaks and add JARs as necessary. I discovered this by finding this exception when I tried executing ServletUnit tests, and after searching all of my JARs and the Google-sphere, I found the reference to tomcat-juli.jar and its expectation to be found; note this is a Tomcat-ism, not something with ServletUnit.
com.meterware.httpunit.HttpInternalErrorException: Error on HTTP request: 500 org.springframework.web.util.NestedServletException: Handler processing failed; nested exception is java.lang.NoClassDefFoundError: org/apache/juli/logging/LogFactory [http://test/Foo/index.htm]
Required ServletUnit JARs
Configuring the test project with the Tomcat library reduces the need for the JAR files contained within the HttpUnit zip file. Now all we need is to copy into our lib folder, and add to our project's build path, the httpunit.jar from the lib folder, and from the jar folder we need the js-version.jar for JavaScript, and the jtidy-version.jar and nekohtml-version.jar and xerces-version.jar for HTML processing. Of course, the "-version" will be replaced with the version information in the file name, and any of these can be exchanged for your favorite if you're using the projects directly for other purposes.
There are no external configuration files that need to be created to make ServletUnit work. Everything else is configured within the tests.
Application and Test Source
There's a lot of potential documentation on using ServletUnit missing from its site. This might be one reason it's not tremendously popular, or why it gets overlooked as a testing tool. Hopefully this will help kick over some of those difficulties and press on to easier testing. Let's start digesting a sample application from the Servlet container's perspective.
Servlet Configuration
In the Foo/WEB-INF/web.xml file, we need to configure our Servlet. Since we're using Spring, we'll start with the out-of-the box easiest DispatcherServlet. The most streamlined configuration is to make a web.xml file that looks like this.
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="Foo" version="2.5"> <display-name>Foo</display-name> <servlet> <servlet-name>foo</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>foo</servlet-name> <url-pattern>*.htm</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.htm</welcome-file> </welcome-file-list> </web-app>
Some will argue that the <web-app> element can be truncated, and certainly that the id attribute doesn't help us. Likewise, the <display-name> isn't used. The magic starts with the <servlet> and the related <servlet-mapping>. The <servlet-mapping> tells us that all *.htm pages reaching our application will be processed by the Servlet with the associated name. Looking at that <servlet> (easy since we've just the one), we see that we're using Spring's DispatcherServlet. With no other configuration, of course, the DispatcherServlet will look for a file named "foo-servlet.xml" (named after the <servlet-name> contents) in the same WEB-INF folder.
Spring Configuration
Our Foo/WEB-INF/foo-servlet.xml is pretty basic, too.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" /> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" /> <context:component-scan base-package="foo.controller" /> </beans>
Here, again, we have the most basic configuration. We need to have a view resolver, so our first <bean> configures the UrlBasedViewResolver, which gives a path (or paths) into which the resolver will look for the JSP pages to use when rendering the results of requests. We're also going to use the annotated controllers ('cause they're cooler), so we've added the mvc.annotation classes that do that, and named the package containing them.
Controller
Jumping, then, to the controller, let's look at the foo.controller package. In our Foo/src folder we'll make a package foo.controller with a single class LoginController in it.
package foo.controller; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import foo.beans.Login; @Controller public class LoginController { protected final static String SESSION_TOKEN = "session_token"; @ModelAttribute("Login") public Login getLogin(@RequestParam(value = "name", required = false) final String name, @RequestParam(value = "password", required = false) final String password) { final Login login = new Login(); login.setName(name); login.setPassword(password); return login; } @RequestMapping(value = { "/index.htm", "/login.htm" }, method = { RequestMethod.GET, RequestMethod.POST }) public String login(final HttpSession httpSession, @ModelAttribute("Login") final Login login) { if (null != login && null != login.getName() && "name".equalsIgnoreCase(login.getName()) && null != login.getPassword() && "password".equalsIgnoreCase(login.getPassword())) { httpSession.setAttribute(SESSION_TOKEN, login.getName()); } return httpSession.getAttribute(SESSION_TOKEN) == null ? "login" : "index"; } }
A fairly trivial controller, we've got a simple method for binding our inputs to a bean, and a simple method for authenticating our login. the getLogin() method takes the optional parameters and gives us a populated bean. The parameters are noted as not required, which allows us to use this for HTTP GET as well, where there might be an Exception thrown if we didn't do the binding and the request was missing the parameters. The login() method does all of the simple work we're doing. All we're doing is adding an attribute to the session if the bean's name and password match. It's a horrible authentication system, to be sure, but it's not the focus here.
Form Bean
Looking at the code, we can see there's a Login bean required. It's really simple, too. A couple of String members, and their setters and getters.
package foo.beans; public class Login { private String name = null; private String password = null; public String getName() { return name; } public String getPassword() { return password; } public void setName(final String name) { this.name = name; } public void setPassword(final String password) { this.password = password; } }
JSPs
Looking at the controller, we can also see that we're passing a couple of view names: "index" and "login" depending on the presence of the session attribute. Since we're using the UrlBasedViewResolver, we have to put JSP pages with those names in the appropriate folder, so in our Foo/WebContent/WEB-INF/jsp folder we create index.jsp and login.jsp.
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Foo Index</title> </head> <body> <div id="index">If you can see this, you must have logged-in.</div> </body> </html>
Above, of course, index.htm, below login.htm.
<%@ page language="java" contentType="text/html; charset=UTF-8"%><%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Foo Login</title> </head> <body> <div id="form"><form:form action="login.htm" commandName="Login" method="POST"> <table> <tr><td>Name:</td><td><form:input path="name" /></td></tr> <tr><td>Password</td><td><form:password path="password" /></td></tr> <tr><td colspan="2"><input id="submit" type="submit" value="Login" /></td></tr> </table> </form:form></div> </body> </html>
Of course, for a real website we'd have more information, probably error handling, style sheets and so on. But, again, we're trying to get to the tests.
Required JARs
Along the way, if you're creating the files in the right order, you may have noticed that the code won't compile as assembled thus far. This is because we're missing our Spring framework libraries. There are four JARs we need. I'm using the Spring 2.5.6 framework with SpringMVC. Pull these JARs from there. Replace with whatever is required for your version of Spring, or other framework, as needed.
- We need to add the spring-webmvc.jar to get the classes in our web.xml and foo-servlet.xml files (won't know that fails immediately, though), and to make our annotations work.
- We need to add the spring.jar to get the satisfy the spring-webmvc.jar and to give us the Controller class.
- We need to add the the commons-logging-api-.jar to satisfy some Spring dependencies.
- We don't know it because of compiler errors, but we can see from our JSP that we're using some JSTL, so we also need the jstl.jar.
After copying those JARs to the Foo/WebContent/WEB-INF/lib folder and recompiling (Eclipse should do that for you), there should be no compile warnings. We can only tell the code works at this point by either trusting the compiler and our ability to parse it, or packaging and deploying it. Since that's just silly, and we're here to discuss ServletUnit, let's kick off some tests.
For the test suite, we need to add a couple JARs as well. This is a little trickier as there's no magic WEB-INF/lib folder from which a classpath will be automatically constructed. As mentioned, I recommend adding a FooTest/lib directory; your preferences may vary. Wherever the libraries are copied, they must be added to the test project's classpath.
- Although Eclipse includes JUnit libraries, it doesn't have a fresh-enough version to support the annotations. As such, copy the junit-4.4.jar (or newer0 to the lib directory and add it to the classpath. Alternatively, do away with the test annotations and use the built-in JUnit.
- Since we're using Spring, we need to add the spring-test.jar to the folder and classpath. This comes from the Spring distribution.
- As mentioned, for the SpringUnit, we need the httpunit.jar from the distribution's lib folder, and the js-version.jar, jtidy-version.jar, nekohtml-version.jar, and xerces-version.jar files from the jar folder.
Tests
Yes, for the really curious, I did actually write the tests while I wrote the above classes and JSPs. Not before, truly driving the development, but as I added logic to the different classes, I made the simple tests reflect the changes. This defends the code against future changes without strictly mandating the construction of the classes.
I've got two test classes, separated only to show their similarities and differences. I don't believe in explicitly testing trivial beans, like the Login class above, but even so, the following tests thoroughly test the bean.
JUnit Test
Here's the kind of unit test we're used to writing. It's a fine test, hitting all of the code.
package foo.controller; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import foo.beans.Login; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/WEB-INF/foo-servlet.xml" }) public class LoginControllerTest { @Autowired private final LoginController loginController = null; @Test public void loginReturnsIndexWhenLoginCorrected() { final Login login = loginController.getLogin("name", "password"); final MockHttpSession mockHttpSession = new MockHttpSession(); assertEquals("index", loginController.login(mockHttpSession, login)); assertEquals(login.getName(), mockHttpSession.getAttribute(LoginController.SESSION_TOKEN)); } @Test public void loginReturnsIndexWhenSessionContainsAttribute() { final MockHttpSession mockHttpSession = new MockHttpSession(); mockHttpSession.setAttribute(LoginController.SESSION_TOKEN, "notNull"); assertEquals("index", loginController.login(mockHttpSession, new Login())); } @Test public void loginReturnsLoginWhenLoginIncorrect() { final Login login = loginController.getLogin("wrong", "silly"); final MockHttpSession mockHttpSession = new MockHttpSession(); assertEquals("login", loginController.login(mockHttpSession, login)); assertNull(mockHttpSession.getAttribute(LoginController.SESSION_TOKEN)); } @Test public void loginReturnsLoginWhenNewSession() { assertEquals("login", loginController.login(new MockHttpSession(), new Login())); } }
A simple, annotated Spring-aware JUnit test case, the LoginControllerTest will verify that the LoginController uses the Login bean as expected, and returns the correct view name. The tests are organized alphabetically, not in any kind of order related to an imaginary session.
The method loginReturnsIndexWhenLoginCorrected() tests to be sure that when our controller's login() method is invoked with a correctly populated bean that the user (presumably) is allowed to view the index page.
The method loginReturnsIndexWhenSessionContainsAttribute() tests to ensure that when the login() method is invoked with a session already containing the attribute that the user is allowed to view the index page.
The method loginReturnsLoginWhenLoginIncorrect() tests to be sure that our simple authentication works with an incorrect combination, and results in the user viewing the login page.
The method loginReturnsLoginWhenNewSession() tests to be sure that our simple authentication works with an empty bean, and results in the user viewing the login page.
Additional tests could be written to test for various combinations of null in the different values. The only assumption made, if checking out the controller, is that the HttpServletSession is always available, as is required by the Servlet spec (and is documented in the Spring documentation). There is a little more null-safety in the controller than our tests require, but that's just my habit.
Note, however, that there's nothing here that tests the annotations or other Spring-isms. Our simple controller doesn't have any auto-wired members or any other kind of members, so really, our controller class could be trimmed down to the following and our tests would still pass.
package foo.controller; import javax.servlet.http.HttpSession; import foo.beans.Login; public class LoginController { protected final static String SESSION_TOKEN = "session_token"; public Login getLogin(final String name, final String password) { final Login login = new Login(); login.setName(name); login.setPassword(password); return login; } public String login(final HttpSession httpSession, final Login login) { if (null != login && null != login.getName() && "name".equalsIgnoreCase(login.getName()) && null != login.getPassword() && "password".equalsIgnoreCase(login.getPassword())) { httpSession.setAttribute(SESSION_TOKEN, login.getName()); } return httpSession.getAttribute(SESSION_TOKEN) == null ? "login" : "index"; } }
ServletUnit Test
Of course, we can see that is not the same intent, and our bean will not be recognized by our annotation-savvy configuration, nor will the Servlet engine direct anything to it. Knowing that we need to have the annotations to make this simple controller function, we should also have a way to test the code so far (not this code, but the code above, with the annotations in place). We could package and deploy the app (even if simply using the Eclipse server tool), fire up a browser and hit the site. That doesn't suit me, as it invariably leads to frustrating log file tracking, injecting logging statements, or debugging tricks. Enter, finally, ServletUnit.
package foo.controller; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import java.io.File; import java.util.Arrays; import javax.servlet.http.HttpSession; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.meterware.httpunit.PostMethodWebRequest; import com.meterware.httpunit.WebForm; import com.meterware.httpunit.WebRequest; import com.meterware.httpunit.WebResponse; import com.meterware.servletunit.InvocationContext; import com.meterware.servletunit.ServletRunner; import com.meterware.servletunit.ServletUnitClient; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/WEB-INF/foo-servlet.xml" }) public class LoginControllerJSPTest { private final static String indexURL = "http://test/Foo/index.htm"; private final static String loginURL = "http://test/Foo/login.htm"; private static ServletRunner servletRunner = null; private ServletUnitClient getServletUnitClient() throws Exception { if (servletRunner == null) { final File webContentRoot = getWebContentRoot(); assertNotNull(webContentRoot); final File webXML = new File(webContentRoot + "/WEB-INF/web.xml"); assertTrue(webXML.exists()); servletRunner = new ServletRunner(webXML.getAbsoluteFile(), "/Foo"); } assertNotNull("Cannot find Foo project", servletRunner); return servletRunner.newClient(); } private File getWebContentRoot() { final String currentPath = System.getProperty("user.dir"); for (File parent = new File(currentPath).getParentFile(); parent.exists(); parent = parent.getParentFile()) { if (Arrays.asList(parent.list()).contains("Foo")) { final File file = new File(parent.getPath() + "/Foo/WebContent"); assertTrue("Cannot find Foo/WebContent", file.exists()); return file.getAbsoluteFile(); } } return null; } @Test public void indexWithoutSessionReturnsLoginForm() throws Exception { final WebResponse webResponse = getServletUnitClient().getResponse(indexURL); assertTrue(webResponse.isHTML()); final String html = webResponse.getText(); assertEquals("Unexpected title: " + html, "Foo Login", webResponse.getTitle()); final WebForm webForm = webResponse.getFormWithID("Login"); assertNotNull("Expected login form: " + html, webForm); assertEquals("login.htm", webForm.getAction()); assertEquals("", webForm.getParameterValue("name")); assertEquals("", webForm.getParameterValue("password")); assertEquals("Login", webForm.getButtonWithID("submit").getValue()); assertNull("Unexpected index div: " + html, webResponse.getElementWithID("index")); } @Test public void loginFailureReturnsLoginPage() throws Exception { final WebRequest webRequest = new PostMethodWebRequest(loginURL); webRequest.setParameter("name", "wrong"); webRequest.setParameter("password", "silly"); final WebResponse webResponse = getServletUnitClient().getResponse(webRequest); assertTrue(webResponse.isHTML()); final String html = webResponse.getText(); assertEquals("Unexpected title: " + html, "Foo Login", webResponse.getTitle()); final WebForm webForm = webResponse.getFormWithID("Login"); assertNotNull("Expected login form: " + html, webForm); assertEquals("login.htm", webForm.getAction()); assertEquals("wrong", webForm.getParameterValue("name")); assertEquals("", webForm.getParameterValue("password")); assertEquals("Login", webForm.getButtonWithID("submit").getValue()); assertNull("Unexpected index div: " + html, webResponse.getElementWithID("index")); } @Test public void loginReturnsIndexPageIfSessionPopulated() throws Exception { final ServletUnitClient servletUnitClient = getServletUnitClient(); WebResponse webResponse = servletUnitClient.getResponse(loginURL); assertTrue(webResponse.isHTML()); final InvocationContext invocationContext = servletUnitClient.newInvocation(loginURL); final HttpSession httpSession = invocationContext.getRequest().getSession(false); assertNotNull(httpSession); assertNull(httpSession.getAttribute(LoginController.SESSION_TOKEN)); httpSession.setAttribute(LoginController.SESSION_TOKEN, "notNull"); webResponse = servletUnitClient.getResponse(loginURL); assertTrue(webResponse.isHTML()); final String html = webResponse.getText(); assertEquals("Unexpected title: " + html, "Foo Index", webResponse.getTitle()); assertNotNull("Expected index div: " + html, webResponse.getElementWithID("index")); assertNull("Unexpected login form: " + html, webResponse.getFormWithID("Login")); } @Test public void loginSuccessReturnsIndexPage() throws Exception { final WebRequest webRequest = new PostMethodWebRequest(loginURL); webRequest.setParameter("name", "name"); webRequest.setParameter("password", "password"); final ServletUnitClient servletUnitClient = getServletUnitClient(); final WebResponse webResponse = servletUnitClient.getResponse(webRequest); assertTrue(webResponse.isHTML()); final String html = webResponse.getText(); assertEquals("Unexpected title: " + html, "Foo Index", webResponse.getTitle()); assertNotNull("Expected index div: " + html, webResponse.getElementWithID("index")); assertNull("Unexpected login form: " + html, webResponse.getFormWithID("Login")); final InvocationContext invocationContext = servletUnitClient.newInvocation(webRequest); final HttpSession httpSession = invocationContext.getRequest().getSession(false); assertEquals("name", httpSession.getAttribute(LoginController.SESSION_TOKEN)); } }
The test is a little larger, but it's both because of the little bit of extra Servlet engine configuration we're doing here, but also because we're trying to learn a little bit about the ServletUnit. Let's step through the test class a bit at a time.
The top bit contains a couple of simple URLs that well use in the test calls. The ServletUnit documentation skimps on this a little bit, so hopefully this helps. The protocol is required to help the server figure out how to handle it. I've not tried "https" or really anything other than "http," but if you leave it out, ServletUnit will complain; according to the documentation, you can use HTTPS, if there's something in your Servlet that knows or cares. At the very least it is used to fill out the information in the HttpRequest and related objects. The server name (here "test") is ignored by ServletUnit, but again is used to populate the HttpRequest and other objects. Likewise the missing, and therefore assumed to be ":80," port number. The next part is the Servlet context (the first "/" after the server name or port, and text to the second "/"), herein "/Foo" which is used to help ServletUnit pass this to the correct Servlet configuration; just hold on to that for a second. Everything after that is the URI path and page name, or in our case "/index.htm" and "/login.htm" which will direct the server to the correct Servlet based on the web.xml <servlet-mapping> elements.
Then we're holding a static ServletRunner. This is for convenience only. It takes a few moments to read the Servlet configuration and build the ServletRunner. This is not an instance of the Servlet engine, but the bit that will give us our faux server when we need one. The first two methods, getServletUnitClient() and getWebContentRoot() do the busy work of prepping the ServletRunner.
The getServletUnitClient() will construct a new client (e.g., our faux web browser) for us. It will build the ServletRunner if necessary, feeding it the web.xml appropraite for the runner. Note in the ServletRunner constructor that we pass it a context name of "/Foo" which matches the context of the requests we're making. This name is what must be used in the URLs when requests are made to this server. The context has nothing to do with our application, as we could be deploying the application with any context name on its eventual server; it must simply match in the URLs we use. As part of the construction of the ServletRunner, we also feed it the File that is our web.xml. This is only one way to construct a ServletRunner, but it is by far the easiest, but that ease does have a price. The ServletRunner will use our File to find the WebContet folder (or whatever it is named) that contains the WEB-INF in which the web.xml file exists; in other words, it assumes that the parent of the directory that has the web.xml file is the root of our web context. There's no way (without rewriting ServletUnit) to get it to find the resources another way; this is fair, as this is really the way most Servlet containers work, but it makes working in the IDE a little trickier.
The getWebContentRoot() method gives us a helping hand in finding that directory. Here's where my separation of base and test code stings a little bit. When the unit test runs, it runs in the root of the encompassing project; in this case in the FooTest/ directory. If the base and test projects were the same, then simply specifying "WebContent/WEB-INF/web.xml" would suffice. Since I'm using (and still recommend) separate projects, we need to go up to our parent and seek our related project. This means that borrowing this code will require changing the "Foo" that it seeks in the if(Arrays...) bit will need to match the appropriate project name. Likewise, it assumes the Eclipse default of "project/WebContent" for the web content directory, so that might need to change if you're doing it differently. Upon successfully finding the WebContent folder, it returns its absolute path, or one of the asserts will fail the test.
Everything discussed to this point could easily be contained in a base test class, allowing for multiple separate test classes to be made without reproducing the busy work of preparing ServletUnit for each client test. With the configuration done, let's make some integration calls!
The method indexWithoutSessionReturnsLoginForm() tests that a request to index.htm will display the login.jsp contents, not the index.jsp contents. A simple GET request is made and parsed by the Dispatcher, running through our controller, ultimately failing our authentication and returning the user to the login page.
The metod loginFailureReturnsLoginPage() tests that a request with incorrect login information fails validation and returns to the login form, with the non-password field (the name) already containing the failed username.
The loginReturnsIndexPageIfSessionPopulated() tests that a request coming in after the session contains the attribute will return to the index page. This one seems a little clunky as it seems that a request needs to be made to complete the server session creation; this is probably not the case, but between the sparce documentation and the fact that making the call twice works, this little workaround does the job. We can confirm with the test that it is our action of setting the attribute and not making the first call that makes it work.
The loginSuccessReturnsIndexPage() tests that a properly populated request passes the authentication and returns the user to the login page, simulating a form submission with a POST.
Additional tests could be written to see what happened with partial form completeness or query strings, but I think the core tests are here; certainly the same tests that the unit tests hit.
Breaking down any of the tests we can see that they're all fairly similar. A ServletUnitClient is created; essentially representing a new session on the server. With the exception of the loginReturnsIndexPageIfSessionPopulated(), only one call is made, as we're largely testing to ensure that the web.xml mappings, Spring configurations and annotations are complete, although there's no reason a sequence couldn't be so tested. The client makes a request using its getResponse() method. While these tests are only trivially using the WebResponse, there's a lot of untapped potential in there, too, such as investigating the response headers and handling other kinds of responses than HTML. The DOM is used in these tests to find the elements named within our expected HTML; for these tests we simply check for the existence of them. Finally, in a couple of the tests, we use the strength of ServletUnit to control or investigate the underlying session in the Servlet.
Debugging Possible
One last nice thing about using ServletUnit is that the test can be debugged, which will at some point jump right into the Servlet. This has incredible value as the little glitches that happen to get in the way can many times be found with very little effort when we can debug. Set a breakpoint at the entry of the controller, or even in the bean binding method, and we can see the execution. Further, set a breakpoint in the JSP, and watch as that's run. No need to start a Servlet container and fire up a browser and try to get the situation right; just make a test that preps the situation, and start debugging.
A great post. Thank you. It helped me much with writing JUnit Tests for my webapp.