Categories
Agile Java Software

An alternative approach to creating specs from JUnit tests

I’m a convert to Behavior-Driven Development, or BDD, as championed by people like Dan North and Dave Astels. As a first step, I wanted to be able to generate a report from an existing collection of JUnit tests which read more like a set of specs (this would be in addition to the normal success/fail/error report), which would in turn be an encouragement to think in terms of behaviour specification when writing tests for new functionality.

To begin with, TestDox looked like just the job. Unfortunately, when I tried it I came across a few things that weren’t quite perfect for my situation:

  • It makes hardcoded assumptions about your test naming convention. For some reason we seem to have developed a local test suite naming convention of *Tests.java, instead of the more normal *Test.java or Test*.java.
  • It generates a single page of output, which gets unwieldy when you have a large number of tests
  • It flattens the package structure, which makes it hard to find the specs for a particular class, especially if there are similarly-named test suites in different packages.
  • It doesn’t include tests/specs which are inherited from a parent class.

While I could have probably solved most of these by modifying the code for TestDox, it got me thinking about whether there might be a different way of doing it. After a few false starts, I settled on the idea of writing a custom stylesheet for the JUnitReport ant task.

What I ended up with was the file below:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
    xmlns:lxslt="http://xml.apache.org/xslt"
    xmlns:redirect="http://xml.apache.org/xalan/redirect"
    xmlns:stringutils="xalan://org.apache.tools.ant.util.StringUtils"
    xmlns:regexp="com.linkwerk.util.Regexp"
    extension-element-prefixes="redirect regexp">
    <xsl:output method="html" indent="yes" encoding="US-ASCII"/>
    <xsl:decimal-format decimal-separator="." grouping-separator=","/>
    <!--
    Copyright 2006 Kerry Buckley, http://www.kerrybuckley.com/
    
    Based on JunitReport stylesheet
    
    Copyright 2001-2005 The Apache Software Foundation
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
    http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    -->
    
    <!--
    
    Spec-generating stylesheet to be used with Ant JUnitReport output.
    
    Requires lw-regexp-util-1.0.0.jar (from http://www.linkwerk.com/pub/xslt/lib/) in the classpath.
    
    -->
    <xsl:param name="output.dir" select="'.'"/>
    
    <xsl:template match="testsuites">
        <!-- create index.html -->
        <redirect:write file="{$output.dir}/index.html">
            <xsl:call-template name="index.html"/>
        </redirect:write>
        
        <!-- create stylesheet.css -->
        <redirect:write file="{$output.dir}/stylesheet.css">
            <xsl:call-template name="stylesheet.css"/>
        </redirect:write>
        
        <!-- create intro.html -->
        <redirect:write file="{$output.dir}/intro.html">
            <xsl:call-template name="intro.html"/>
        </redirect:write>
        
        <!-- create packages.html -->
        <redirect:write file="{$output.dir}/packages.html">
            <xsl:apply-templates select="." mode="packages"/>
        </redirect:write>
        
        <!-- process all packages -->
        <xsl:for-each
            select="./testsuite[not(./@package = preceding-sibling::testsuite/@package)]">
            <xsl:variable name="name">
                <xsl:call-template name="package-name">
                    <xsl:with-param name="name" select="@package"/>
                </xsl:call-template>
            </xsl:variable>
            <redirect:write file="{$output.dir}/{$name}.html">
                <xsl:call-template name="package">
                    <xsl:with-param name="name" select="@package"/>
                </xsl:call-template>
            </redirect:write>
        </xsl:for-each>
    </xsl:template>
    
    <xsl:template name="index.html">
        <html>
            <head>
                <title>Behaviour specifications</title>
            </head>
            <frameset cols="20%,80%">
                <frame src="packages.html" name="packageListFrame"/>
                <frame src="intro.html" name="packageFrame"/>
                <noframes>
                    <h2>Frame Alert</h2>
                    <p> This document is designed to be viewed using the frames feature. If
                        you see this message, you are using a non-frame-capable web
                        client. </p>
                </noframes>
            </frameset>
        </html>
    </xsl:template>
    
    <xsl:template name="stylesheet.css">
        <![CDATA[body {
    font:normal 68% verdana,arial,helvetica;
    color:#000000;
}
table tr td, table tr th {
    font-size: 68%;
}

p {
    line-height:1.5em;
    margin-top:0.5em; margin-bottom:1.0em;
}
h1 {
    margin: 0px 0px 5px; font: 165% verdana,arial,helvetica
}
h2 {
    margin-top: 1em; margin-bottom: 0.5em; font: bold 125% verdana,arial,helvetica
}]]>
    </xsl:template>
    
    <xsl:template name="intro.html">
        <html>
            <head>
                <link rel="stylesheet" type="text/css" title="Style"
                    href="stylesheet.css"/>
                <title>Behaviour specifications</title>
            </head>
            <body>
                <h1>Behaviour specifications</h1>
                <xsl:call-template name="pageHeader"/>
                <p>Select a package on the left to view the specifications for classes in
                    that package.</p>
            </body>
        </html>
    </xsl:template>
    
    <xsl:template match="testsuites" mode="packages">
        <html>
            <head>
                <link rel="stylesheet" type="text/css" title="Style"
                    href="stylesheet.css"/>
                <title>Packages</title>
            </head>
            <body>
                <h2>Packages</h2>
                <table width="100%">
                    <xsl:apply-templates
                        select="testsuite[not(./@package = preceding-sibling::testsuite/@package)]"
                        mode="all.packages">
                        <xsl:sort select="@package"/>
                    </xsl:apply-templates>
                </table>
            </body>
        </html>
    </xsl:template>
    
    <xsl:template match="testsuite" mode="all.packages">
        <tr>
            <td nowrap="nowrap">
                <xsl:element name="a">
                    <xsl:attribute name="href">
                        <xsl:call-template name="package-name">
                            <xsl:with-param name="name" select="@package"/>
                        </xsl:call-template>
                        <xsl:text>.html</xsl:text>
                    </xsl:attribute>
                    <xsl:attribute name="target">packageFrame</xsl:attribute>
                    <xsl:call-template name="package-name">
                        <xsl:with-param name="name" select="@package"/>
                    </xsl:call-template>
                </xsl:element>
            </td>
        </tr>
    </xsl:template>
    
    <xsl:template name="package">
        <xsl:param name="name"/>
        <xsl:variable name="local-name">
            <xsl:call-template name="package-name">
                <xsl:with-param name="name" select="$name"/>
            </xsl:call-template>
        </xsl:variable>
        <html>
            <head>
                <link rel="stylesheet" type="text/css" title="Style"
                    href="stylesheet.css"/>
                <title>Behaviour specifications:
                    <xsl:value-of select="$local-name"/></title>
            </head>
            <body>
                <h1>Behaviour specifications: <code>
                    <xsl:value-of select="$local-name"/></code></h1>
                <xsl:call-template name="pageHeader"/>
                <xsl:apply-templates
                    select="/testsuites/testsuite[@package = $name]"/>
            </body>
        </html>
    </xsl:template>
    
    <xsl:template match="testsuite">
        <h2>
            <xsl:call-template name="prettify-suite">
                <xsl:with-param name="name" select="@name"/>
            </xsl:call-template>
        </h2>
        <ul>
            <xsl:apply-templates select="./testcase"/>
        </ul>
    </xsl:template>
    
    <xsl:template match="testcase">
        <li>
            <xsl:call-template name="prettify-test">
                <xsl:with-param name="name" select="@name"/>
            </xsl:call-template>
        </li>
    </xsl:template>
    
    <!-- Page header -->
    <xsl:template name="pageHeader">
        <p>Derived from reports generated by
            <a href="http://www.junit.org/">JUnit</a> and
            <a href="http://jakarta.apache.org/">Ant</a>.</p>
        <hr size="1"/>
    </xsl:template>
    
    <!-- replace the package name with "default-package" if it is empty -->
    <xsl:template name="package-name">
        <xsl:param name="name"/>
        <xsl:value-of select="$name"/>
        <xsl:if test="$name = ''">default-package</xsl:if>
    </xsl:template>
    
    <!-- turn a test suite name into a human-readable phrase -->
    <xsl:template name="prettify-suite">
        <xsl:param name="name"/>
        
        <!-- remove leading "Test" or trailing "Test[s]" from classname (adjust for your naming convention) -->
        <xsl:variable name="suite">
            <xsl:value-of
                select="regexp:replace(regexp:replace(string($name), '^Test', '', ''), 'Tests?$', '', '')"/>
        </xsl:variable>
        <!-- split into separate words -->
        <xsl:variable name="context">
            <xsl:call-template name="separate-words">
                <xsl:with-param name="str" select="$suite"/>
            </xsl:call-template>
        </xsl:variable>
        <!-- Convert all but first character to lower case -->
        <xsl:value-of select="substring($context, 1, 1)"/>
        <xsl:call-template name="lower-case">
            <xsl:with-param name="str" select="substring($context, 2)"/>
        </xsl:call-template>
    </xsl:template>
    
    <!-- turn a test name into a human-readable phrase -->
    <xsl:template name="prettify-test">
        <xsl:param name="name"/>
        <!-- remove leading "test" -->
        <xsl:variable name="test">
            <xsl:value-of
                select="regexp:replace(string($name), '^test', '', '')"/>
        </xsl:variable>
        <!-- split into separate words -->
        <xsl:variable name="spec">
            <xsl:call-template name="separate-words">
                <xsl:with-param name="str" select="$test"/>
            </xsl:call-template>
        </xsl:variable>
        <!-- Convert to lower case -->
        <xsl:call-template name="lower-case">
            <xsl:with-param name="str" select="$spec"/>
        </xsl:call-template>
    </xsl:template>
    
    
    <!-- convert a camelCase string to separate words (treat multiple consecutive digits as a word) -->
    <xsl:template name="separate-words">
        <xsl:param name="str"/>
        <xsl:value-of
            select="normalize-space(regexp:replace(regexp:replace(string($str), '([A-Z])', 'g', ' $1'), '([^0-9])([0-9])', 'g', '$1 $2'))"/>
    </xsl:template>
    
    <!-- convert a string to lower case -->
    <xsl:template name="lower-case">
        <xsl:param name="str"/>
        <xsl:value-of
            select="translate($str, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')"/>
    </xsl:template>
    
</xsl:stylesheet>

To use it, simply save junit-frames.xsl to a convenient directory, and point your ant JunitReport task to that directory (xslt in the example below). If you’ve already run a “normal” JunitReport, you’ll need to tell it to ignore the suites file that this creates:

<junitreport>
    <fileset dir="reports/junit" includes="*xml" excludes="TESTS-TestSuites.xml"/>
    <report format="frames" styledir="xslt" todir="reports/spec"  excludes="TESTS-TestSuites.xml"/>
</junitreport>

The only slight gotcha is that it relies on the Linkwerk regular expression extension, which means you need to have lw-regexp-util-1.0.0.jar in your classpath when you run ant (either by copying it to $ANT_HOME/lib, or by having it in the system classpath when you run ant). As far as I can figure, you can’t work round this using a taskdef, because of the same classloader issues that mean you always end up copying junit.jar into ant’s lib dir too.

What you should get is a report with the package names listed in the left frame, linked to a page for each package containing its specs. So for example, if you a test suite like this:

package org.whoever.foo;

import junit.framework.TestCase;

public class TestANewlyInitialisedFoo extends TestCase {

    public void testShouldHaveZeroLength() {
        ...
    }

    public void testShouldHaveNoMembers() {
        ...
    }

    public void testShouldThrowExceptionOnPop() {
        ...
    }
}

You’ll end up with a section in the org.whoever.foo report looking a bit like this:

A newly initialised foo

  • should have zero length
  • should have no members
  • should throw exception on pop

Here’s a screenshot of the actual output. Don’t pay too much attention to the actual test names, most of which weren’t written using a BDD approach.

picture-1.png

5 replies on “An alternative approach to creating specs from JUnit tests”

[…] Venendo ad un po’ di codice, il primo passo per incamminarsi su questa strada può essere quello di utilizzare un semplice xsl sui report prodotti da JUnit, per cominciare a misurare quanto ad oggi, siamo orientati alle specifiche ed ai comportamenti, invece che alle classi ed ai metodi. Tale xsl assume che ogni test JUnit sia una collezione di specifiche, ad esempio scritta un questo modo: Java [Show Styled Code]: […]

Don’t you think it’s idiocy to use 2000-feet-high divs with horizontal scrollbar below? To scroll horizontally it’s first lines, one must scroll the page down, and then up.

Yeah, it’s not ideal. Do you know of a better syntax highlighting plugin for WordPress?

Of course, it’s not an issue if you have a Mighty Mouse ;-)

Don’t you think it’s idiocy to use 2000-feet-high divs with horizontal scrollbar below?

OK, I’ve tweaked the CSS for IG Syntax Highlighter a little from the defaults. Hopefully that’s better.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.