Monday, December 27, 2010

Unit testing Freemarker macros with JUnit and HtmlUnit

I recently had to modify a pagination macro in FreeMarker. That pagination system is a bit different from the ordinary ones, and the safest way to make sure that it was working properly was to unit test it. Making a lot of data to test it would have been to much hastle, so I looked for a way to test the FreeMarker macro out of the web container. I wanted to make sure that the generated page numbers were correct, and also that the previous and next arrows were displayed when needed. All pages are html anchors, all contained into a div tag. To test it, I had to :
  1. Make a unit test class for the macro.
  2. Make FreeMarker interprete the macro.
  3. Confirm that the expected html elements like divs and anchors were present.
Why using HtmlUnit ? Because it has some useful methods to retrieve html elements by their name or by their id.

Getting the necessary libraries

All I needed was : JUnit, FreeMarker and HtmlUnit. HtmlUnit has quite a lot of dependencies, but all the necessary libraries are part of the HtmlUnit archive. You can easy go to their respective homepage and download them yourself, or get them via Maven. Here are the necessary dependencies for the sample we'll make in this tutorial :
<dependencies>
    <dependency>
      <groupId>net.sourceforge.htmlunit</groupId>
      <artifactId>htmlunit</artifactId>
      <version>2.8</version>
    </dependency>
 <dependency>
     <groupId>org.freemarker</groupId>
     <artifactId>freemarker</artifactId>
     <version>2.3.16</version>
 </dependency>
 <dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.8.2</version>
 </dependency>
  </dependencies>

A basic macro

Let's make a basic pagination macro. We'll keep it simple. The page links won't point anywhere. We just want to show page numbers. The current page will be plain text, other page numbers will be html anchors. The macro parameters will be the total number of pages and the current page. It will also show previous and next if necessary.
<#macro doPagination totalPages currentPage >
 <#if (totalPages > 1)>
  <div id="pagination">
   <#-- Previous page -->
   <#if (currentPage > 1)>
    <a href="#">Prev</a>
   </#if>
   
   <#-- Page number -->
   <#list 1 .. totalPages as pageNumber>
    <#if (pageNumber == currentPage)>
     <div id="currentPage">${pageNumber}</div>
    <#else>
     <div><a href="#">${pageNumber}</a></div>
    </#if>  
   </#list>
   
   <#-- Next page -->  
   <#if (currentPage < totalPages)>
    <a href="#">Next</a>
   </#if>  
  </div>
 </#if>
</#macro>

Using a dummy template

We need a FreeMarker template to use the pagination macro. This template will just import the file where the macro is, and call the macro.
<#import "/main/webapp/templates/macros/pagination.ftl" as pagination/>
<html>
<body>
<@pagination.doPagination totalPages currentPage />
</body>
</html>

Preparing FreeMarker for the tests

We'll use the FreeMarker API to convert the dummy template into html. We'll make use of the following two classes : freemarker.template.Configuration and freemarker.template.Template. Let's set this up in the @Before method of our test class.
public class PaginationTest {

 /** Root directory where the FreeMarker templates are */
    private static final String TEMPLATE_ROOT = "src";
    /** Dummy pagination template location */
    private static final String PAGINATION_TEMPLATE = "test/java/com/kuriqoo/templates/macros/pagination_test.ftl";

    private Configuration cfg;
    private Template template;

    private Map<String, Object> rootMap;
    
    @Before
    public void setUp() throws Exception {
        cfg = new Configuration();
        cfg.setDirectoryForTemplateLoading(new File(TEMPLATE_ROOT));

        template = cfg.getTemplate(PAGINATION_TEMPLATE);
        
        rootMap = new HashMap<String, Object>();
    }
}
First, the TEMPLATE_ROOT. The macro is in "/src/main/webapp/templates/macros/pagination.ftl" and the dummy template is in "/src/test/java/com/kuriqoo/templates/macros/pagination_test.ftl". So the common template root is "src".

Then, the PAGINATION_TEMPLATE. This points to the dummy template. Note that the root ("src") is not included in it.


We create a Configuration and set its template root directory. Using that Configuration instance, we can build some templates, by calling its getTemplate method. Here, we are making our dummy template. Note that this template is not yet trnasformed into html. This will come after.

The HashMap call rootMap is used by FreeMarker when it generates the html code from the template. This map must contain all the necessary information used by our macro, like the total number of pages and the current page. This information will be set in the test methods.


Unit testing

Each test will be executed with the following steps:
  1. Set the information used by the pagination macro in the root map.
  2. Make FreeMarker generate html from our template.
  3. Feed HtmlUnit with our html to generate an HtmlPage instance.
  4. Declare some expectations, like the expected anchor tags representing page numbers.
  5. Get the anchor tags present in the pagination div and compare them to the expected ones.
  6. Assert that the current page is present and is not a link.

Set the information used by the pagination macro in the root map

Our simple macro only need to know the number of pages and the current page, so we'll set that information in the root map used by FreeMarker :
private void setPaginationAttributes(int totalPages, int currentPage) {
        rootMap.put("totalPages", totalPages);
        rootMap.put("currentPage", currentPage);
    }

Make FreeMarker generate html from our template and feed HtmlUnit with our html to generate an HtmlPage instance

This is the interesting part. Remember the Template instance we created above ? We'll use it to generate html. The method used for this is process(Map, Writer). We'll output the html content into a StringWriter. The reason is that we want to use the output to generate an HtmlUnit HtmlPage.
private HtmlPage getHtmlPageFromTemplate() throws TemplateException, IOException {

    // Process template
    StringWriter out = new StringWriter();
    template.process(rootMap, out);

    // Get HtmlPage
    WebClient client = new WebClient();
    PageCreator pageCreator = client.getPageCreator();
    WebResponseData responseData = new WebResponseData(out.toString().getBytes(), 200, "OK", new ArrayList<NameValuePair>());
    WebResponse response = new WebResponse(responseData, new URL("http://localhost:8080/"), HttpMethod.GET, 10);
    return (HtmlPage)pageCreator.createPage(response, client.getCurrentWindow());
}
There's a lot going on here, but most of it is dummy data. First, we call the process method of the Template method, and set the ouput into a StringWriter. Then, in order to create an HtmlPage, we setup a WebClient and generate a dummy response containing our html.

Declare some expectations

We want to check the following:
  1. All page numbers are anchors tags, except the current page
  2. The previous and next anchors tags are present when necessary
  3. The number of generated anchor tags equals the number of expected anchor tags
  4. The current page is present, and is not an anchor tag
Here is an example where there are only two pages, and the first one is the current page.
@Test
public void testPagination_twopages_currentisone() throws IOException, TemplateException {
    // Make FreeMarker template
 setPaginationAttributes(2, 1);
    HtmlPage page = getHtmlPageFromTemplate();

    // page links
    List<String> expectedContent = makeExpectedLinks(new String[]{"2"}, false, true); 
    List<HtmlElement> pageLinks = getPaginationLinks(page);
    Assert.assertEquals(expectedContent.size(), pageLinks.size());
    assertContainsLinks(expectedContent, pageLinks);
    assertCurrentPageIs(page, "1");
}
First, we set our FreeMarker information and generate the HtmlPage : two pages, page one is the current page. Then, we declare some expectations. makeExpectedLinks is a method generating the expected anchor links text. We pass an array of page numbers, and whether or not the previous and next links should be present:
private List<String> makeExpectedLinks(String[] links, boolean prev, boolean next) {
    List<String> expectedContent = new ArrayList<String>();
    expectedContent.addAll(Arrays.asList(links));
    if ( prev ) {
        expectedContent.add("Prev");
    }
    if ( next ) {
        expectedContent.add("Next");
    }
            
    return expectedContent;
}
The getPaginationLinks extracts the anchor tags from the html content. This is where HtmlUnit becomes useful. Here, we look for the pagination div and get all anchor elements.
private List<HtmlElement> getPaginationLinks(HtmlPage page) {
    HtmlDivision div = page.getHtmlElementById("pagination");
    return div.getElementsByTagName("a");
}
We make sure that the number of links meets our expectations : Assert.assertEquals(expectedContent.size(), pageLinks.size());. Then the content of the links are checked via the assertContainsLinks method :
private void assertContainsLinks(List<String> expectedLinks, List<HtmlElement> anchors) {
    List<String> anchorsText = new ArrayList<String>();
    for (HtmlElement htmlElement : anchors) {
        anchorsText.add(htmlElement.getTextContent());            
    }
    for( String expectedLink : expectedLinks ) {
        Assert.assertTrue("Expected page link["+expectedLink+"] was not found", anchorsText.contains(expectedLink));
    }
}

The current page is also checked via the assertCurrentPageIs method :
private void assertCurrentPageIs(HtmlPage page, String pageNo) {
 HtmlElement currentPage = getPaginationCurrent(page);
 Assert.assertTrue(!(currentPage instanceof HtmlLink));
    Assert.assertEquals(pageNo, currentPage.getTextContent());
}

From there, it's easy to make other tests for different page numbers and current pages:

@Test
    public void testPagination_twopages_currentistwo() throws IOException, TemplateException {
        // Make FreeMarker template
     setPaginationAttributes(2, 2);
        HtmlPage page = getHtmlPageFromTemplate();

        // page links
        List<String> expectedContent = makeExpectedLinks(new String[]{"1"}, true, false); 
        List<HtmlElement> pageLinks = getPaginationLinks(page);
        Assert.assertEquals(expectedContent.size(), pageLinks.size());
        assertContainsLinks(expectedContent, pageLinks);
        assertCurrentPageIs(page, "2");
    }

    @Test
    public void testPagination_threepages_currentistwo() throws IOException, TemplateException {
        // Make FreeMarker template
     setPaginationAttributes(3, 2);
        HtmlPage page = getHtmlPageFromTemplate();

        // page links
        List<String> expectedContent = makeExpectedLinks(new String[]{"1","3"}, true, true); 
        List<HtmlElement> pageLinks = getPaginationLinks(page);
        Assert.assertEquals(expectedContent.size(), pageLinks.size());
        assertContainsLinks(expectedContent, pageLinks);
        assertCurrentPageIs(page, "2");
    }

Running the tests under Eclipse shows the expected green bar :

Conclusion

Making FreeMarker macros and checking the content via a browser can be annoying and time wasting. The more branches you have in your logic, the more data you have to create. Changing the logic afterwards quickly becomes dangerous, unless you reuse the same data as before to make sure you've not broken the original logic. Unit testing the macro using JUnit, FreeMarker and HtmlUnit allows to prevent these problems.

2 comments:

  1. Thanks for the post, I am techno savvy. I believe you hit the nail right on the head. I am highly impressed with your blog. It is very nicely explained. Your article adds best knowledge to our Java EE Training in Chennai. or learn thru Java EE Training in Chennai Students.

    ReplyDelete