An HTML Test Runner (with code)

Dale King
2001-10-17
2001-10-17
  • Dale King
    Dale King
    2001-10-17

    I adapted the TextTestRunner to create and HtmlTestRunner. The problem with the TextTestRunner is that the format of the output is not very clean. I didn't want to go to the MFC version, because frankly it has its problems too (e.g. Can't resize the window to see the failures).

    So to create nice output I changed the test runner to output the errors and failures as HTML tables. This can certainly be improved upon and that is why I am posting it here. Hopefully someone else will find it usefull and take the ball and run with it.

    Here is my code to use it and under Windows if the tests fail to open the HTML file in a browser:

        using namespace CppUnit;

        std::ofstream htmlFile( FILENAME );

        HtmlTestRunner runner;

        runner.addTest( TestFactoryRegistry::getRegistry().makeTest() );

        if( !runner.run( htmlFile ) )
        {
            ShellExecute( NULL, TEXT( "open" ), TEXT( FILENAME ), NULL, NULL, SW_SHOWMAXIMIZED );
        }

    HtmlTestRunner.h
    ----------------
    #ifndef CPPUNIT_HTMLTESTRUNNER_H
    #define CPPUNIT_HTMLTESTRUNNER_H

    #include <string>
    #include <iostream>
    #include <vector>
    #include <cppunit\Test.h>
    #include <cppunit\TestSuite.h>

    /**
    * An HTML test runner.
    *
    * The test runner manage the life cycle of the added tests.
    *
    * The test runner can run only one of the added tests or all the tests.
    *
    * TestRunner prints out a trace as the tests are executed followed by a
    * summary at the end.
    */
    class HtmlTestRunner
    {
    public:
        HtmlTestRunner();
        virtual ~HtmlTestRunner();

        bool run( std::ostream &output, std::string testName ="" );

        void addTest( CppUnit::Test *test );

    protected:
        bool runTest( CppUnit::Test *test, std::ostream &output );
        bool runTestByName( std::string testName, std::ostream &output );

        CppUnit::Test *findTestByName( std::string name ) const;
        CppUnit::TestSuite *suite;
    };

    #endif  // HTMLTESTRUNNER_H

    HtmlTestRunner.cpp
    ------------------
    #include <cppunit/TestSuite.h>

    #include "HtmlTestRunner.h"
    #include "HtmlTestResult.h"

    HtmlTestRunner::HtmlTestRunner()
    {
        suite = new CppUnit::TestSuite( "All Tests" );
    }

    HtmlTestRunner::~HtmlTestRunner()
    {
        delete suite;
    }

    void
    HtmlTestRunner::addTest( CppUnit::Test *test )
    {
        if ( test != NULL )
            suite->addTest( test );
    }

    /** Runs the named test case.
    *
    * \param testName Name of the test case to run. If an empty is given, then
    *                 all added test are run. The name must be the name of
    *                 of an added test.
    * \param doWait if \c true then the user must press the RETURN key
    *               before the run() method exit.
    */

    bool
    HtmlTestRunner::run( std::ostream &output, std::string testName )
    {
        return runTestByName( testName, output );
    }

    bool
    HtmlTestRunner::runTestByName( std::string testName, std::ostream &output )
    {
        if ( testName.empty() )
        {
            return runTest( suite, output );
        }
        else
        {
            CppUnit::Test *test = findTestByName( testName );
            if ( test != NULL )
                return runTest( test, output );
            else
            {
                std::cout << "Test " << testName << " not found." << std::endl;
                return false;
            }
        }
    }

    CppUnit::Test *
    HtmlTestRunner::findTestByName( std::string name ) const
    {
        for ( std::vector<CppUnit::Test *>::const_iterator it = suite->getTests().begin();
        it != suite->getTests().end();
        ++it )
        {
            CppUnit::Test *test = *it;
            if ( test->getName() == name )
                return test;
        }
        return NULL;
    }

    bool
    HtmlTestRunner::runTest( CppUnit::Test *test, std::ostream &output )
    {
        HtmlTestResult result;
        test->run( &result );
       
        if (result.wasSuccessful ())
            std::cerr << std::endl << "OK (" << result.runTests () << " tests)" << std::endl;
        else
            std::cerr << std::endl
            << "!!!FAILURES!!!" << std::endl
            << "Test Results:" << std::endl
            << "Run:  "
            << result.runTests ()
            << "   Failures: "
            << result.testFailures ()
            << "   Errors: "
            << result.testErrors ()
            << std::endl;
       
        output << result << std::endl;
       
        return result.wasSuccessful();
    }

    HtmlTestResult.h
    ----------------
    #ifndef HTMLTESTRESULT_H
    #define HTMLTESTRESULT_H

    #include <iostream>
    #include <cppunit/TestResult.h>

      class Exception;
      class Test;

      class HtmlTestResult : public CppUnit::TestResult
      {
        public:
          virtual void        addError      ( CppUnit::Test *test, CppUnit::Exception *e);
          virtual void        addFailure    ( CppUnit::Test *test, CppUnit::Exception *e);
          virtual void        startTest     ( CppUnit::Test *test);
          virtual void        endTest       ( CppUnit::Test *test);
          virtual void        print         (std::ostream& stream);
          virtual void        printErrors   (std::ostream& stream);
          virtual void        printFailures (std::ostream& stream);
          virtual void        printHeader   (std::ostream& stream);     
          virtual void        printTrailer  (std::ostream& stream);     
      };

      /** insertion operator for easy output */
      std::ostream& operator<< (std::ostream& stream, HtmlTestResult& result);

    #endif // HTMLTESTRESULT_H

    HtmlTestResult.cpp
    ------------------
    #include <iostream>
    #include "HtmlTestResult.h"
    #include "cppunit/Exception.h"
    #include "cppunit/NotEqualException.h"
    #include "cppunit/Test.h"
    #include <iomanip>

    class QuoteHtml
    {
    private:
        const std::string& text;
       
    public:
        QuoteHtml( const std::string& textToQuote )
            : text( textToQuote )
        {
        }
       
        friend std::ostream&
            operator<< (std::ostream& stream, QuoteHtml& quoted )
        {
            for( std::string::const_iterator i = quoted.text.begin(); i != quoted.text.end(); i++ )
            {
                switch( *i )
                {
                case '<' :
                    stream << "&lt;";
                    break;
                case '>' :
                    stream << "&gt;";
                    break;
                case '&' :
                    stream << "&amp;";
                    break;
                case '\&quot;' :
                    stream << "&quot;";
                    break;
                default:
                    stream << *i;
                    break;
                }
            }
            return stream;
        }
    };

    std::ostream&
    operator<< (std::ostream& stream, HtmlTestResult& result)
    {
        result.print (stream); return stream;
    }

    void
    HtmlTestResult::addError (CppUnit::Test *test, CppUnit::Exception *e)
    {
        TestResult::addError (test, e);
        std::cerr << " <Error>";
    }

    void
    HtmlTestResult::addFailure (CppUnit::Test *test, CppUnit::Exception *e)
    {
        TestResult::addFailure (test, e);
        std::cerr << " -Failed-";
    }

    void
    HtmlTestResult::startTest (CppUnit::Test *test)
    {
        TestResult::startTest (test);
        std::cerr << test->getName();
    }

    void
    HtmlTestResult::endTest (CppUnit::Test *test)
    {
        TestResult::endTest (test);
        std::cerr << std::endl;
    }

    void
    HtmlTestResult::printErrors (std::ostream& stream)
    {
        if ( testErrors() != 0 )
        {
            stream << "<H2>Errors:</H2>";
           
            if (testErrors() == 1)
                stream << "There was 1 Error:<P>" << std::endl;
            else
                stream << "There were " << testErrors() << " errors: <P>" << std::endl;
            int i = 1;
           
            stream << "<TABLE BORDER=\&quot;1\&quot; CELLPADDING=\&quot;3\&quot; CELLSPACING=\&quot;0\&quot; WIDTH=\&quot;100%\&quot;>" << std::endl;
            stream << "<TR><TH>Test</TH><TH>Error</TH><TH>File</TH><TH ALIGN=\&quot;CENTER\&quot;>Line</TH></TR>" << std::endl;
            for (std::vector<CppUnit::TestFailure *>::iterator it = errors ().begin (); it != errors ().end (); ++it) {
                CppUnit::TestFailure* failure = *it;
                CppUnit::Exception* e = failure->thrownException ();
               
                stream << "<tr>" << std::endl;
               
                stream << "<td>" << std::endl
                       << QuoteHtml( failure->failedTest()->getName() )
                       << "</td>" << std::endl;
               
                stream << "<td>" << std::endl;
               
                stream << QuoteHtml( failure->thrownException ()->what() );
                stream << "</td>" << std::endl;
               
                if ( e )
                {
                    stream << "<TD>" << QuoteHtml( e->fileName() )
                        << "</TD><TD ALIGN=\&quot;CENTER\&quot;>" << e->lineNumber()
                        << "</TD>" << std::endl;
                }
                else
                {
                    stream << "<TD></TD><TD></TD>" << std::endl;
                }
               
                stream << "</tr>" << std::endl;
               
                i++;
            }
            stream << "</table>" << std::endl;
        }
    }

    void
    HtmlTestResult::printFailures (std::ostream& stream)
    {
        stream << "<H2>Failures:</H2>";
       
        if ( testFailures() == 0 )
        {
            stream << "No failures" << std::endl;
        }
        else
        {
            if (testFailures () == 1)
                stream << "There was 1 failure:<P>" << std::endl;
            else
                stream << "There were " << testFailures () << " failures: <P>" << std::endl;
           
            int i = 1;
           
            stream << "<TABLE BORDER=\&quot;1\&quot; CELLPADDING=\&quot;3\&quot; CELLSPACING=\&quot;0\&quot; WIDTH=\&quot;100%\&quot;>" << std::endl;
            stream << "<TR><TH>Test</TH><TH>Failure</TH><TH>File</TH><TH ALIGN=\&quot;CENTER\&quot;>Line</TH></TR>" << std::endl;
            for (std::vector<CppUnit::TestFailure *>::iterator it = failures ().begin (); it != failures ().end (); ++it) {
                CppUnit::TestFailure* failure = *it;
                CppUnit::Exception* e = failure->thrownException();
               
                stream << "<tr>" << std::endl;
               
                stream << "<td>" << std::endl
                    << QuoteHtml( failure->failedTest()->getName() )
                    << "</td>" << std::endl;
                  
                stream << "<td>" << std::endl;
               
                if ( failure->thrownException()->isInstanceOf( CppUnit::NotEqualException::type() ) )
                {
                    CppUnit::NotEqualException *e = (CppUnit::NotEqualException*)failure->thrownException();
                    stream << "expected: " << QuoteHtml( e->expectedValue() ) << "<BR>"
                        << "but was:  " << QuoteHtml( e->actualValue() );
                }
                else
                {
                    stream << QuoteHtml( failure->thrownException ()->what() );
                }
                stream << "</td>" << std::endl;
               
                if ( e )
                {
                    stream << "<TD>" << QuoteHtml( e->fileName() )
                        << "</TD><TD ALIGN=\&quot;CENTER\&quot;>" << e->lineNumber()
                        << "</TD>" << std::endl;
                }
                else
                {
                    stream << "<TD></TD><TD></TD>" << std::endl;
                }
               
                stream << "</tr>" << std::endl;
                i++;
            }
           
            stream << "</table>" << std::endl;
        }
    }

    void
    HtmlTestResult::print (std::ostream& stream)
    {
        printHeader (stream);
        printErrors (stream);
        printFailures (stream);
        printTrailer (stream);
    }

    void
    HtmlTestResult::printHeader (std::ostream& stream)
    {
        stream << "<HTML>" << std::endl;
        stream << "<HEAD>" << std::endl;
        stream << "<TITLE>CppUnit Test Results</TITLE>" << std::endl;
        stream << "</HEAD>" << std::endl;
        stream << "<BODY>" << std::endl;
        stream << "<h1>Test Results</h1>" << std::endl;
       
        stream << "There were " << runTests() << " tests ran." << std::endl;
    }

    void
    HtmlTestResult::printTrailer (std::ostream& stream)
    {
        stream << "</BODY>" << std::endl;
        stream << "</HTML>" << std::endl;
    }