In this instalment on assembly testing [1] I’ll talk about how automated system tests run over my Java web applications. The set-up is rather cheap, not very labour-intensive and can incrementally evolve from a simple assembly test (aka smoke test) to a full-fledged system test. Since this post is rather concrete naming tools and frameworks, I’ll present the actors up-front: I’ll show how to run end-to-end tests on a Spring web application during a Maven build with the tests involving the user interface (HTML + Javascript), the HTTP communication with the Java backend, the Spring application consisting of controllers and services and finally the database. A quick note on naming: in this post the terms “end-to-end tests” and “system tests” are synonyms.
The accompanying source code for this project is on Github [9] at https://github.com/ggeorgovassilis/test-with-embedded-tomcat
Running end-to-end tests during the build cycle
- Maven first resolves dependencies and compiles the application
- It then runs unit tests in the test phase
- The application WAR is built in the package phase
- Integration tests are then run in the integration-test phase. That’s where we’ll run an embedded Tomcat and interact with the application through the HTMLUnit [8] browser.
- Since the scope of this document is end-to-end testing we’ll not look at anything that happens after the testing phase like deployment and report generation
Getting Maven dependencies right and declaring integration tests in pom.xml
Dependencies for this endeavour are surprisingly hard to get right since most modules have runtime dependencies on each other but don’t explicitly declare them in the project’s pom.xml (for an up-to-date version go to github). We’ll need the embedded Tomcat, Htmlunit and also will have to sort out logging.
Next, we tell Maven to run all tests during the normal testing phase except those in the integration package and only run the integration tests during the integration-test phase:
<plugin> <!-- Separates the unit tests from the integration tests. --> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.20</version> <configuration> <skip>true</skip> <trimStackTrace>false</trimStackTrace> <configuration> <useSystemClassLoader>${useSystemClassloader}</useSystemClassLoader> </configuration> </configuration> <executions> <execution> <id>unit-tests</id> <phase>test</phase> <goals> <goal>test</goal> </goals> <configuration> <skip>false</skip> <configuration> <useSystemClassLoader>${useSystemClassloader}</useSystemClassLoader> </configuration> <includes> <include>**/*Test.java</include> </includes> <excludes> <exclude>**/integrationtests/*</exclude> </excludes> </configuration> </execution> <execution> <id>integration-tests</id> <phase>integration-test</phase> <goals> <goal>test</goal> </goals> <configuration> <skip>false</skip> <configuration> <useSystemClassLoader>${useSystemClassloader}</useSystemClassLoader> </configuration> <includes> <include>**/integrationtests/*Test.java</include> </includes> </configuration> </execution> </executions> </plugin>
The “useSystemClassloader” setting is surefire a property which comes in handy when solving class loading issues.
System test starts Tomcat and runs HTMLUnit
We’ll write a base integration test which takes care of running Tomcat, the web application and the HTMLUnit browser. Concrete tests can extend the base test class and interact with the web application. The complete code for the base class is on Github, the interesting parts:
Configures Tomcat to load the application from where maven copied it to and assigns a temporary location for the workspace.
String webappDirLocation = "target/test-with-embedded-tomcat-0.0.2-SNAPSHOT"; tomcat = new Tomcat(); File temp = File.createTempFile("integration-test", ".tmp"); tomcat.setBaseDir(temp.getParent()); tomcat.setPort(SERVER_PORT);
// disable scanning of dependencies introduced by maven StandardJarScanner discardingJarScanner = new StandardJarScanner(); discardingJarScanner.setJarScanFilter(new JarScanFilter() { public boolean check(JarScanType jarScanType, String jarName) { return jarName.contains("jstl-"); } }); context.setJarScanner(discardingJarScanner);
// enable JNDI, e.g. for server-provided data sources tomcat.enableNaming(); ContextResource resource = new ContextResource(); resource.setName("jdbc/twetDataSource"); resource.setAuth("Container"); resource.setType(javax.sql.DataSource.class.getName()); resource.setScope("Sharable"); resource.setProperty("driverClassName", "org.hsqldb.jdbc.JDBCDriver"); resource.setProperty("url", "jdbc:hsqldb:mem:testdb"); context.getNamingResources().addResource(resource); tomcat.getServer().getGlobalNamingResources().addResource(resource);
A concrete test: visiting the home page
@Category(IntegrationTest.class) public class TestLoginForm extends BaseIntegrationTest{ final String homePage = "http://localhost:"+SERVER_PORT+"/mywebapp/"; @Test public void test_homepage_correct_date_and_headlines() throws Exception{ Date now = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy"); String expectedDate = sdf.format(now); HtmlPage page = webClient.getPage(baseUrl); List list = page.getByXPath("//div[@class='today']"); assertEquals(1, list.size()); HtmlDivision div = (HtmlDivision)list.get(0); assertTrue(div.asText().contains(expectedDate)); list = page.getByXPath("//div[contains(@class,'headline')]/b"); assertEquals("Twetland election polls", ((HtmlElement)list.get(0)).getTextContent()); } }
The test can obviously be extended towards more complex interactions with the web application.
Things to watch out for
IDEs and Maven: the integration testing approach described here vitally depends on the application being properly built and deployed in Maven’s target path – many IDEs, including Eclipse, use their own compilation and deployment mechanism which does not update the target folder. Thus, while unit tests will work fine, integration tests will yield wrong results unless a maven clean install is first executed.
Last not least, the inclusion of the embedded Tomcat introduces several classpath dependencies like logging libraries or Tomcat’s EL implementation which may affect the application’s behaviour. I noticed a slightly different behaviour in resource mappings when upgrading from 8.0.28 to 8.0.30.
Resources
[1] Assembly testing
http://georgovassilis.blogspot.com/2016/01/assembly-tests.html
[2] Maven build lifecycle
https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
[3] Adding datasource programmaticaly to JNDI context in embedded tomcat 7
http://stackoverflow.com/questions/20860283/adding-datasource-programmaticaly-to-jndi-context-in-embedded-tomcat-7
[4] Spring profiles
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-profiles.html
[5] Feature toggles
http://martinfowler.com/bliki/FeatureToggle.html
[6] Configuring Surefire
http://maven.apache.org/surefire/maven-surefire-plugin/examples/configuring-classpath.html
[7] Duplicate persistence unit
http://stackoverflow.com/questions/11656848/conflicting-persistence-unit-definitions
[8] HTMLUnit
http://htmlunit.sourceforge.net/
[9] test-with-embedded-tomcat
https://github.com/ggeorgovassilis/test-with-embedded-tomcat
Some updates to data source handling
LikeLike
I think most of these stuff nowadays if you use spring-boot-test and spring-boot you have far more cleaner setup and utilizing MockMVC simplifies your life…
LikeLike
Absolutely! TDD is a really good technique with many successful tools around it like the ones you listed that cover test design, test automation, dependency management and dependency mocking. This post is just a technical implementation of an older post [1] where I tried to make the point that if a project doesn't want to afford decent testing it should at least implement a smoke test since the investment is minimal and the return is huge.
[1] http://georgovassilis.blogspot.com/2016/01/assembly-tests.html
LikeLike
By the way, my favourite tool for integration tests is Arquillian http://arquillian.org/
LikeLike