Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
HomeAnnouncementsWhite Papers
Discussion GroupsFirst AidDatabasesJavaBeansGUIJava 3DVirtual MachineCORBASecurityToolsGeneral
Java DirectoryOpen Source ProjectsSample Book ChaptersUser GroupsWeb Resources
Related Topics
Databases.NETMore Topics ...

Java Forum / General / January 2006

Tip: Looking for answers? Try searching our database.

JUnit - HTML report

Thread view: 
Jacob - 17 Jan 2006 09:11 GMT
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:jacob.dreyer@geosoft.nospam">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[i].getClassName();
      if (!className.startsWith("junit")) {
        message_.append(stackTrace[i].getClassName());
        message_.append(" ");
        if (sourceDirectory_ != null)
          message_.append(toLink(stackTrace[i].getClassName(),
                                 stackTrace[i].getFileName(),
                                 stackTrace[i].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();
  }
}
Andrew McDonagh - 17 Jan 2006 23:20 GMT
> 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
[quoted text clipped - 14 lines]
>
> 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
Jacob - 19 Jan 2006 07:52 GMT
> 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
Andrew McDonagh - 19 Jan 2006 19:46 GMT
>> Whilst its appreciated...I see 2 main problems with this...
>>
[quoted text clipped - 5 lines]
> 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.

>> 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.

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.
Jacob - 20 Jan 2006 08:03 GMT
> 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 :-)


Free Magazines

Get these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...

Oracle MagazineNetwork ComputingComputer WorldBio-IT WorldeWeekInformation WeekInfosecurity
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2009 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.