JUnit - HTML report

J

Jacob

As HTML report generation from JUnit supprisingly appears to be an Ant
feature only (refer to an earlier post on this subject), I supply my
home brewed version of this as promised (my appologize for the long
listing :)

Some of the code is based on ideas from junit-addons as documented in
the source. Writing a custom report is probably not done by the book
due to the very poor documentation and strange (and quite fat) API of
JUnit.

Disclaimer: It was written in a rush and can probably be improved in a
number of ways.

Usage:

java HtmlTestReport /my/path/to/classes > test-report.html


Jacob




import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import junit.framework.Test;
import junit.framework.TestSuite;
import junit.framework.TestResult;
import junit.framework.TestFailure;
import junit.framework.AssertionFailedError;
import junit.framework.TestListener;



/**
* Class for creating a HTML JUnit test report.
*
* @author <a href="mailto:[email protected]">Jacob Dreyer</a>
*/
public final class HtmlTestReport implements TestListener
{
/** Background color of header */
private final String HEADER_COLOR = "\"#ccccff\"";

/** Background color of report */
private final String BACKGROUND_COLOR = "\"#ffffff\"";

/** Background color of successful tests */
private final String SUCCESS_COLOR = "\"#99ff99\"";

/** Background color of failed tests */
private final String FAILURE_COLOR = "\"#ff9999\"";

/** Directory of source root */
private final File sourceDirectory_;

/** Directory of classes root */
private final File classDirectory_;

/** Overall start time for report generation */
private long time0_;

/** Start time for current test */
private long startTime_;

/** Error message of current test */
private StringBuffer message_;

/** Total number of succeeding tests */
private int nSuccess_ = 0;

/** Total number of failed tests */
private int nFailed_ = 0;



/**
* Create a HTML report instance. Typical usage:
* <pre>
* File sourceDir = new File("/home/joe/dev/src");
* File classDir = new File("/home/joe/dev/classes");
*
* HtmlTestReport report = new HtmlTestReport(sourceDir, classDir);
* report.print();
* </pre>
* The HTML report is written to stdout and will typically be
* redirected to a .html file.
*
* @param sourceDirectory Root directory of source. If specified, it
* will be used to create a link to the source
* file of failing classes. May be null.
* @param classDirectory Root directory of classes.
*/
public HtmlTestReport(File sourceDirectory, File classDirectory)
{
assert classDirectory != null : "Illegal class directory";

sourceDirectory_ = sourceDirectory;
classDirectory_ = classDirectory;

time0_ = System.currentTimeMillis();
}



/**
* Print the HTML report to standard out.
*/
public void print()
{
try {
// Extract the test suite
TestSuite suite = HtmlTestReport.build(classDirectory_);

// Print report header
printHeader();

// Loop through all the tests and make the report run the test
// and capture the result
for (int i = 0; i < suite.testCount(); i++) {
Test test = suite.testAt(i);
run(test);
}

// Print report footer
printFooter();
}
catch (IOException exception) {
exception.printStackTrace();
}
}



/**
* Convert the specified number of milliseconds into a readable
* string in the form [nSeconds].[nMilliseconds]
*
* @param nMillis Number of milliseconds to convert.
* @return String representation of the input.
*/
private static String getTime(long nMillis)
{
assert nMillis >= 0 : "Illegal time specification";

long nSeconds = nMillis / 1000;
nMillis -= nSeconds * 1000;
StringBuffer time = new StringBuffer();
time.append(nSeconds);
time.append('.');
if (nMillis < 100) time.append('0');
if (nMillis < 10) time.append('0');
time.append(nMillis);

return time.toString();
}



/**
* Print the HTML report header.
*/
private void printHeader()
{
System.out.println("<html>");
System.out.println("<table" +
" cellpadding=\"5\"" +
" cellspacing=\"0\"" +
" bgcolor=" + BACKGROUND_COLOR +
" border=\"1\"" +
" width=\"100%\"" +
">");
System.out.println(" <tr>");
System.out.println(" <td bgcolor=" + HEADER_COLOR + " colspan=2>" +
"<font size=+2>" +
"<b>Test</b>" +
"</font>" +
"</td>");
System.out.println(" <td bgcolor=" + HEADER_COLOR + ">" +
"<font size=+2>" +
"<b>Time</b>" +
"</font>" +
"</td>");
System.out.println(" <td align=\"right\" bgcolor=" + HEADER_COLOR + ">" +
"<font size=+2>" +
"<b>" + new Date() + "</b>" +
"</font>" +
"</td>");
System.out.println(" </tr>");
}



/**
* Print the HTML report footer.
*/
private void printFooter()
{
int nTotal = nSuccess_ + nFailed_;

String color = nFailed_ == 0 ? SUCCESS_COLOR : FAILURE_COLOR;

System.out.println(" <tr>");
System.out.println(" <td colspan=3> &nbsp; </td>");
System.out.println(" <td bgcolor=" + color + ">");

System.out.println(" <table border=0 cellspacing=0 cellpadding=0>");

System.out.println(" <tr>");
System.out.println(" <td><font size=+0 face=\"helvetica\"><b>Failed</b></font></td>");
System.out.println(" <td><font size=+0 face=\"courier\">" +
nFailed_ +
" (" + (double) nFailed_ / nTotal * 100.0 + "%)" +
"</font>" +
"</td>");
System.out.println(" </tr>");

System.out.println(" <tr>");
System.out.println(" <td><font size=+0 face=\"helvetica\"><b>Success &nbsp; &nbsp; </b></font></td>");
System.out.println(" <td><font size=+0 face=\"courier\">" +
nSuccess_ +
" (" + (double) nSuccess_ / nTotal * 100.0 + "%)" +
"</font>" +
"</td>");
System.out.println(" </tr>");

System.out.println(" <tr>");
System.out.println(" <td><font size=+0 face=\"helvetica\"><b>Total</b></font></td>");
System.out.println(" <td><font size=+0 face=\"courier\">" +
nTotal +
"</font>" +
"</td>");
System.out.println(" </tr>");

System.out.println(" <tr>");
System.out.println(" <td><font size=+0 face=\"helvetica\"><b>Time</b></font></td>");
System.out.println(" <td><font size=+0 face=\"courier\">" +
HtmlTestReport.getTime(System.currentTimeMillis() - time0_) +
"</td>");
System.out.println(" </tr>");

System.out.println(" </table>");
System.out.println(" </td>");
System.out.println(" </tr>");

System.out.println("</table>");
System.out.println("</html>");
}



/**
* Run the specified test and capture the result in the report.
*
* @param test Test to run.
*/
private void run(Test test)
{
System.out.println(" <tr>");
System.out.println(" <td colspan=4>" +
"<font face=\"courier\">" +
"<b>" + test.toString() + "</b>" +
"</font>" +
"</td>");
System.out.println(" </tr>");

TestResult result = new TestResult();
result.addListener(this);

test.run(result);
}



/**
* Create an anchor tag from the specified class and file name.
*
* @param className Fully specified class name of class to link to.
* @param fileName File where the class resides.
* @param lineNo Line number to scroll to (TODO: not currently used).
* @return Anchor string of the form "<a href='link'>file</a>".
*/
private String toLink(String className, String fileName, int lineNo)
{
assert sourceDirectory_ != null : "Source not available";
assert className != null : "Missing class name";
assert fileName != null : "Missing file name";
assert lineNo >= 0 : "Illegal line number spcifier";


String base = sourceDirectory_.toString() + "/";
String packageName = className.substring(0, className.lastIndexOf('.'));

String link = "<a href=\"" +
base +
packageName.replace(".", "/") +
"/" +
fileName +
"\">" +
fileName + ":" + lineNo +
"</a>";

return link;
}



/**
* Add the appropriate report content for a starting test.
* This method is called by JUnit when a test is about to start.
*
* @param test Test that is about to start.
*/
public void startTest(Test test)
{
assert test != null : "Test cannot be null";

startTime_ = System.currentTimeMillis();
message_ = new StringBuffer();

String name = test.toString();
String testName = name.substring(0, name.indexOf('('));

System.out.println(" <tr>");
System.out.println(" <td> &nbsp; &nbsp; </td>");
System.out.println(" <td valign=\"top\"><font face=\"courier\"><b>" + testName +
"</b></font></td>");
}



/**
* Add the appropriate report content for a failed test.
* This method is called by JUnit when a test fails.
*
* @param test Test that is failing.
* @param throwable Throwable indicating the failure
*/
public void addError(Test test, Throwable throwable)
{
assert test != null : "Test cannot be null";
assert throwable != null : "Exception must be specified for failing test";

StackTraceElement stackTrace[] = throwable.getStackTrace();
for (int i = 0; i < stackTrace.length; i++) {
String className = stackTrace.getClassName();
if (!className.startsWith("junit")) {
message_.append(stackTrace.getClassName());
message_.append(" ");
if (sourceDirectory_ != null)
message_.append(toLink(stackTrace.getClassName(),
stackTrace.getFileName(),
stackTrace.getLineNumber()));
message_.append("<br>");
break;
}
}

message_.append(" &nbsp; &nbsp; " + throwable.getMessage());
}



/**
* Add the appropriate report content for a failed test.
* This method is called by JUnit when a test fails.
*
* @param test Test that is failing.
* @param throwable Throwable indicating the failure
*/
public void addFailure(Test test, AssertionFailedError error)
{
assert test != null : "Test cannot be null";
assert error != null : "Exception must be specified for failing test";

// Treat failures and errors the same
addError(test, error);
}



/**
* Add the appropriate report content for the end of a test.
* This method is called by JUnit when a test is done.
*
* @param test Test that is done.
*/
public void endTest(Test test)
{
assert test != null : "Test cannot be null";

// Compute the test duration
long time = System.currentTimeMillis() - startTime_;

System.out.println(" <td valign=\"top\"><font face=\"courier\"> " +
HtmlTestReport.getTime(time) +
"</font></td>");

// Test was a success
if (message_.length() == 0) {
nSuccess_++;
System.out.println(" <td bgcolor=" + SUCCESS_COLOR +
"<font face=\"helvetica\"><b>Success</b></font></td>");
}

// Test failed
else {
nFailed_++;
System.out.println(" <td bgcolor=" + FAILURE_COLOR + ">" +
message_.toString() +
"</td>");
}

System.out.println(" </tr>");
}



/**
* Method for creating a test suite from all tests
* found in the present directory and its sub directories.
*
* @param directory Root directory for test search.
* @return Test suite compund of all tests found.
*/
private static TestSuite build(File directory)
throws IOException
{
assert directory != null : "Directory cannot be null";

TestSuite suite = new TestSuite(directory.getName());

// Get list of all classes in the path
List<String> classNames = HtmlTestReport.getAllClasses(directory);

for (String className : classNames) {
try {
Class clazz = Class.forName(className);

// Filter out all non-test classes
if (junit.framework.TestCase.class.isAssignableFrom(clazz)) {

// Because a 'suite' method doesn't always exist in a TestCase,
// we need to use the try/catch so that tests can also be
// automatically extracted
try {
Method suiteMethod = clazz.getMethod("suite", new Class[0]);
Test test = (Test) suiteMethod.invoke(null);
suite.addTest(test);
}
catch (NoSuchMethodException exception) {
suite.addTest(new TestSuite(clazz));
}
catch (IllegalAccessException exception) {
exception.printStackTrace();
// Ignore
}
catch (InvocationTargetException exception) {
exception.printStackTrace();
// Ignore
}
}
}
catch (ClassNotFoundException exception) {
// Ignore
}
}

return suite;
}



/**
* Retrieve all classes from the specified path.
*
* @param root Root of directory of where to search for classes.
* @return List of classes on the form "com.company.ClassName".
*/
private static List<String> getAllClasses(File root)
throws IOException
{
assert root != null : "Root cannot be null";

// Prepare the return array
List<String> classNames = new ArrayList<String>();

// Get all classes recursively
String path = root.getCanonicalPath();
HtmlTestReport.getAllClasses(root, path.length() + 1, classNames);

return classNames;
}



/**
* Retrive all classes from the specified path.
*
* @param root Root of directory of where to search for classes.
* @param prefixLength Index into root path name of path considered.
* @param result Array to add classes found
*/
private static void getAllClasses(File root, int prefixLength,
List<String> result)
throws IOException
{
assert root != null : "Root cannot be null";
assert prefixLength >= 0 : "Illegal index specifier";
assert result != null : "Missing return array";

// Scan all entries in the directory
for (File entry : root.listFiles()) {

// If the entry is a directory, get classes recursively
if (entry.isDirectory()) {
if (entry.canRead())
getAllClasses(entry, prefixLength, result);
}

// Entry is a file. Filter out non-classes and inner classes
else {
String path = entry.getPath();
boolean isClass = path.endsWith(".class") && path.indexOf("$") < 0;
if (isClass) {
String name = entry.getCanonicalPath().substring(prefixLength);
String className = name.replace(File.separatorChar, '.').
substring(0, name.length() - 6);
result.add(className);
}
}
}
}



/**
* A JUnit HTML report generator. Typical usage:
* <pre>
* java HtmlTestReport /home/joe/dev/classes > test.html
* </pre>
*
* @param arguments Comand line arguments.
*/
public static void main(String[] arguments)
{
// Parse command line arguments
if (arguments.length < 1 || arguments.length > 2) {
System.err.println("Usage: HtmlTestReport classDir [sourceDir]");
}

File classDirectory = new File(arguments[0]);

File sourceDirectory = null;
if (arguments.length == 2)
sourceDirectory = new File(arguments[1]);

// Create a HTML report instance
HtmlTestReport report = new HtmlTestReport(sourceDirectory,
classDirectory);

// Print report to standard out
report.print();
}
}
 
A

Andrew McDonagh

Jacob said:
As HTML report generation from JUnit supprisingly appears to be an Ant
feature only (refer to an earlier post on this subject), I supply my
home brewed version of this as promised (my appologize for the long
listing :)

Some of the code is based on ideas from junit-addons as documented in
the source. Writing a custom report is probably not done by the book
due to the very poor documentation and strange (and quite fat) API of
JUnit.

Disclaimer: It was written in a rush and can probably be improved in a
number of ways.

Usage:

java HtmlTestReport /my/path/to/classes > test-report.html


Jacob

Whilst its appreciated...I see 2 main problems with this...

1) Where are its own unit tests?

2) Its Java 1.5 where as JUnit (and most people using it) is 1.4.x
 
J

Jacob

Andrew said:
Whilst its appreciated...I see 2 main problems with this...

1) Where are its own unit tests?

As this is free software you don't necesseriliy get what you
want, but what the creator needs and had the time to complete.

If you are willing to pay, you can certainly forward your
requirements. Otherwise I suggest you add on to the module
what you feel is mising and then publish your improved version.
2) Its Java 1.5 where as JUnit (and most people using it) is 1.4.x

Again, I suggest you back-port to 1.4 and publish your version.

(And I'd like a reference to the "most people using it"-survey
for exact numbers).


Jacob
 
A

Andrew McDonagh

Jacob said:
As this is free software you don't necesseriliy get what you
want, but what the creator needs and had the time to complete.

If you are willing to pay, you can certainly forward your
requirements.

Yes we all know this.


Otherwise I suggest you add on to the module
what you feel is mising and then publish your improved version.

The point of JUnit is to unit test code...JUnit itself is covered with
tests, and almost all add ons and extensions are summarily unit tested.
Again, I suggest you back-port to 1.4 and publish your version.

I don't need the feature at all, I was merely stating that creating an
add-on for a free tool that is not able to be used by the majority of
the free tools user is a waste.

If the op wants to use J1.5, then their time might be better spent
creating the add on for JUnit 4.0 which is just about to be released.
JUnit 4.0 makes full use of J1.5 - indeed its a complete rewrite, using
annotations instead of base classes for test case/suite identification.
(And I'd like a reference to the "most people using it"-survey
for exact numbers).

Like most NG posts, there are no numbers to back up the claim, only
empirical evidence that one gets from being in the general community
that uses the tool. If you like you could always create a survey on the
yahoo Junit list, to tell us.

http://groups.yahoo.com/group/junit/

There's 6605 members currently...should give us a good representative
picture of JDK version.
 
J

Jacob

Andrew said:
I don't need the feature at all, I was merely stating that creating an
add-on for a free tool that is not able to be used by the majority of
the free tools user is a waste.

It's not a waste at all. *I* needed the feature, and *I* happen to
work with jdk1.5 and I created the feature for *myself*, and it
fulfills my needs exactly.

As I found that it could be useful for others I posted it here.
I don't really care about the community "majority", if nobody can
benefit from the initiative that's fine. If you don't like it, or
can't use it, or find it insufficient in some way then please don't
care about it :)
 
Joined
Jul 27, 2009
Messages
1
Reaction score
0
New to Junit

Usage: HtmlTestReport classDir [sourceDir]
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at Sam.main(Sam.java:536)

hi, i tried to compile the same report. but getting the above mentioned error.

Waiting for u r help ASAP
Regards,
/S
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,057
Latest member
KetoBeezACVGummies

Latest Threads

Top