title | image |
---|---|
Testing |
../images/testing.jpg |
When this application was simple and being developed as one project, just clicking around provided some ad hoc testing. Refactoring the code into a library and creating a continuos build environment, also requires automated regression tests. Tapestry supports both unit testing and integration testing.
Over the years, the testing framework has changed. The current direction is Geb/Spock for integration testing. Since Spock is built on JUnit, it's simplest to use JUnit for unit testing. It's possible to mix and match, but doing so may require additional Pom file configuration. The standard procedure would be to write some unit testsm then move on to integration testing. But after the refactoring, there are no control structures and not much code left to unit test.
The best place to start is testing the main control flow. In this case, a user should be able to search for a hotel and book it. It's possible to just jump right in and create a couple of tests, but Geb provides a better way.
One big problem with Web integration testing is that small markup changes sometimes fail many test cases for no reason. Geb addresses this issue with Page and Module classes. The page class provides an abstraction layer between the tests and the page markup.
For example, the page class can export a header which is contained in an h1 tag. The test can now just refer to the header and if the markup later changes to h2, only the page class needs updating. Modules are used to represent common functionality across pages. For example a Grid module could represent a Tapestry grid and could be made from Grid row, header and pagination modules. The isolates the page class form the markup in a component.
So with that out of the way create a test for the HotelIndex page. For now, all this test needs to do it click on the first hotel link. Unfortunately this results in a login page, not the booking page, so that needs to handled.
Next we need a test to fill out the booking form, and finally a test of the confirmation page.
class HotelIndexPage extends Page {
static url = "hotel";
//static at = { title == "Hotels" };
static content = {
heading { $("h1").text() }
book { $("a", text: "Book") }
}
}
class HotelIndexSpec extends GebReportingSpec {
@Shared
def runner;
def setupSpec() {
runner = new Jetty7Runner("src/main/webapp", "/hotel", 8080, 8081);
runner.start()
}
def cleanupSpec() {
runner.stop()
}
def "Hotel Index"() {
when: to HotelIndexPage;
book.click()
and:
page(LoginPage)
username.value("bfb")
password.value("bfb")
loginButton.click()
page(BookingNewPage)
checkinDate.value("01/01/2020")
checkoutDate.value("01/02/2020")
beds.value("1");
bookButton.click()
page(BookingViewPage)
then:
heading == "View"
}
}
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.9</version>
<configuration>
<!-- Sets the VM argument line used when unit tests are run. -->
<argLine>${surefireArgLine}</argLine>
<!-- Skips unit tests if the value of skip.unit.tests property is true -->
<!-- Excludes integration tests when unit tests are run. -->
<includes>
<include>**/*Spec.*</include>
<include>**/*Test.*</include>
</includes>
<systemPropertyVariables>
<tapestry.execution-mode>IntegrationMode</tapestry.execution-mode>
<geb.build.reportsDir>target/test-reports/geb</geb.build.reportsDir>
<tapestry.compiled-asset-cache-dir>target/classes</tapestry.compiled-asset-cache-dir>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.gmaven</groupId>
<artifactId>gmaven-plugin</artifactId>
<version>1.4</version>
<configuration>
<providerSelection>2.0</providerSelection>
</configuration>
<executions>
<execution>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.5.201505241946</version>
<configuration>
<excludes>
<exclude>**/documentation/**</exclude>
<exclude>**/modules/**</exclude>
</excludes>
</configuration>
<executions>
<!-- Prepares the property pointing to the JaCoCo runtime agent which
is passed as VM argument when Maven the Surefire plugin is executed. -->
<execution>
<id>pre-unit-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<!-- Sets the path to the file which contains the execution data. -->
<destFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</destFile>
<!-- Sets the name of the property containing the settings for JaCoCo
runtime agent. -->
<propertyName>surefireArgLine</propertyName>
</configuration>
</execution>
<!-- Ensures that the code coverage report for unit tests is created
after unit tests have been run. -->
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<!-- Sets the path to the file which contains the execution data. -->
<dataFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</dataFile>
<!-- Sets the output directory for the code coverage report. -->
<outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
These tests are pretty basic, but they do test the main flow of the application. Bugs are inevitable and it looks bad when the main point of the application is broken. So what kind of coverage do these tests provide? There are code coverage reporting tools that will give some insight into code quality and coverage.
JaCoCo is a free code coverage tool that can work with test cases. Getting everything configured in the Pom is a bit tricky, but the results are interesting. First of all the report contains a lines of code count, while the count is a bit above zero there are a couple of interesting things revealed.
First there are about 100 methods and less than 200 lines of code. There can't be many lines of code per method.
A second and perhaps more interesting thing: the complexity number is equal to the method count. That means the app has no branches or loops. That makes test case code coverage easier but unfortunately there is an issue. Since Tapestry creates classes on the fly, the code coverage reported for pages/components is 0 even if it's tested.
All the more reason to keep code out of pages. Services created with binder.bind are also not included.
The simplest way to test pages/components/mixins is unit tests. They are easy to create, run fast and help nail down the interface between the java and tml files. Since the fields can be protected, tests created in the same package have easy access for testing.
Services can be created and inserted into injected fields, and render methods can be called. It's possible to use some kind of mocking tool to create fake services but sometimes it's just as easy to create testing services. In the case of the DAO, a simple in memory database could be useful for testing and development. To create one, start with an implementation that contains a map of class and list. This simulates tables by class and the queries just return everything.
Since the pages have almost no code, they are easy to test. Whenever this is not the case it's probably time to move the code into a service.
public class DAOsImple implements DAO {
private final Map<Class<?>,List<Object>> entities = new HashMap<Class<?>,List<Object>>();
public DAOsImple(Object ... entities) {
for ( Object entity : entities ) {
save(entity);
}
}
public DAOsImple() {
}
@Override
public void save(Object entity) {
List<Object> list = entities.get(entity.getClass());
if ( list == null ) {
list = new ArrayList<>();
entities.put(entity.getClass(), list);
}
list.add(entity);
}
private <E> E one(Class<E> clazz) {
List<E> l = many(clazz);
if ( l.isEmpty() ) {
return null;
}
return l.get(0);
}
private <E> List<E> many(Class<E> clazz) {
@SuppressWarnings("unchecked")
List<E> l = (List<E>) entities.get(clazz);
if ( l == null ) {
return Collections.emptyList();
}
return l;
}
@Override
public <E> E findById(Class<E> clazz, Serializable id, boolean lock) {
return one(clazz);
}
@Override
public <E> List<E> findAll(Class<E> clazz) {
return many(clazz);
}
@Override
public <E> E findByQuery(Class<E> clazz, String queryString, Object... objects) {
return one(clazz);
}
@Override
public <E> List<E> query(Class<E> clazz, String queryString, Object... objects) {
return many(clazz);
}
@Override
public <E> List<E> namedQuery(Class<E> clazz, String queryName, Object... objects) {
return many(clazz);
}
@Override
public <E> E findByNamedQuery(Class<E> clazz, String queryName, Object... objects) {
return one(clazz);
}
}
public class HotelIndexTest {
@Test
public void setupRender() {
HotelIndex index = new HotelIndex();
index.dao = new DAOsImple(new Hotel());
index.setupRender();
assertTrue(index.hotels.size() == 1);
assertTrue(index.hotel == null);
}
}
The services built with Autobuild have code coverage but no live coding in development. In order to get code coverage in testing, it's possible to override the services with ones built using Autobuild. This can be done by create a new module for use during testing.
@ImportModule({TapestryDocumentationModule.class,HotelBookingDocumentationModule.class,JacquardModule.class})
public class IntegrationModule {
public static void bind(ServiceBinder binder) {
binder.bind(DAO.class, new ServiceBuilder<DAO>() {
@Override
public DAO buildService(ServiceResources resources) {
// TODO Auto-generated method stub
return resources.autobuild(HibernateDAO.class);
}
}).withId("testDAO");
binder.bind(UserService.class, new ServiceBuilder<UserService>() {
@Override
public UserService buildService(ServiceResources resources) {
// TODO Auto-generated method stub
return resources.autobuild(UserServiceImpl.class);
}
}).withId("testUserService");
binder.bind(HotelService.class, new ServiceBuilder<HotelService>() {
@Override
public HotelService buildService(ServiceResources resources) {
// TODO Auto-generated method stub
return resources.autobuild(HotelServiceImpl.class);
}
}).withId("testHotelService");
binder.bind(EmailService.class, new ServiceBuilder<EmailService>() {
@Override
public EmailService buildService(ServiceResources resources) {
// TODO Auto-generated method stub
return resources.autobuild(SendEmailLogger.class);
}
}).withId("testEmailService");
binder.bind(Listing.class, new ServiceBuilder<Listing>() {
@Override
public Listing buildService(ServiceResources resources) {
// TODO Auto-generated method stub
return resources.autobuild(ListingImpl.class);
}
}).withId("testListngService");
}
public static void contributeApplicationDefaults(MappedConfiguration<String, Object> configuration) {
//configuration.override(SymbolConstants.APPLICATION_VERSION, "1.0-SNAPSHOT-DEV");
}
@Contribute(ServiceOverride.class)
public static void setupApplicationServiceOverrides(@SuppressWarnings("rawtypes") MappedConfiguration<Class,Object> configuration,
@Local DAO dao, @Local UserService userService, @Local HotelService hotelService,
@Local EmailService emailService, @Local Listing listing)
{
configuration.add(DAO.class, dao);
configuration.add(UserService.class, userService);
configuration.add(HotelService.class, hotelService);
configuration.add(EmailService.class, emailService);
configuration.add(Listing.class, listing);
}
}
Libraries also need testing, but there is no application to test. The simplest way around this is to write one.
To start, create pages in src/test, run them as an application and view them with a browser. Since these pages are in the test branch of the source tree, they are not included in the jar file. Next, write Geb tests for the test application. An interesting thing about this approach is that Tapestry page development is very efficient, but Geb test case development is slow because each code change requires a server restart. Since the library pages can to anything, the trick is to create a generic test case and push the work into the pages.
Running the test app just requires setting up a path to the webapp directory.