12
\$\begingroup\$

I've written a minimal unit test framework. The goal was to allow assertions of booleans, for equality and catched exceptions. Two things are bugging the most. That assertions have to rely on macros and using a function pointer type instead of one of the more modern C++ ways. Are there ways to further minimize? What C++14 features could help?

For execution the test unit must derive from tiny::Unit. See an example and repository here

Header:

#include <stdexcept>
#include <string>
#include <sstream>
#include <vector>

/**
* Assertion macros for boolean expressions or equality tests.
*/
#define TINY_ASSERT_OK(expr) tiny::assertOk(expr, #expr, __FILE__, __LINE__)
#define TINY_ASSERT_EQUAL(actual, expected) tiny::assertEqual(actual, expected, __FILE__, __LINE__)

/**
* Assertion macros for expected exceptions
*/
#define TINY_ASSERT_TRY(dummy) do { bool tiny_exception = false; try {
#define TINY_ASSERT_CATCH(type) } catch (const type &ex) { tiny_exception = true; } if( tiny_exception == false ) { tiny::handleMissedException(#type, __FILE__, __LINE__); } } while(false)

namespace tiny
{
    /**
    * Function pointer type for test cases
    */
    typedef void (*TestFunc) (void);

    /**
    * A Unit contains test cases. Every test case has to be registered.
    */
    class Unit
    {
    public:
        Unit(const std::string& name);

        void registerTest(TestFunc foo, const std::string& testName);
        unsigned runTests();
    protected:

    private:
        struct TestCase
        {
            TestFunc foo;
            std::string name;
        };

        Unit(); // = delete
        Unit(const Unit& other); // = delete

        std::vector< TestCase > m_testCases;

        std::string m_name;
    };

    /**
    * Exception if a test failed, other exceptions must be catched or
    * the test will fail.
    */
    class TestFailed : public std::exception
    {
    public:
        TestFailed(const std::string& msg)
        : m_message(msg)
        {}

        virtual ~TestFailed() throw() {}

        virtual const char* what() const throw()
        {
            return m_message.c_str();
        }

    private:
        TestFailed();
        std::string m_message;
    };

    /**
    * Basic assertion for testing
    */
    void assertOk(bool expr, const char* rep, const char* filename, unsigned line);

    /**
    * Assertion for equality
    */
    template< typename U, typename V >
    void assertEqual(const U& actual, const V& expected, const char* filename, unsigned line)
    {
        if( !(actual == expected) )
        {
            std::ostringstream msg;
            msg << filename << ":" << line
                << ": Not equal. Expected=<" << expected << "> Actual=<" << actual << ">";
            throw TestFailed( msg.str() );
        }
    }

    void handleMissedException(const std::string& type, const char* filename, unsigned line);
}

Implementation:

#include "tiny-unit.hpp"
#include <iostream>

namespace
{
    /**
    * Force the unit vector to be initialzed before
    * registering any units.
    */
    struct TinyUnit
    {
        std::vector< tiny::Unit* > unitTests;
    };

    TinyUnit& TINY()
    {
        static TinyUnit tinyUnit;
        return tinyUnit;
    }

    void registerUnit(tiny::Unit* pUnit)
    {
        TINY().unitTests.push_back(pUnit);
    }
}

namespace tiny
{
    /**
    * On construction register this unit to run.
    */
    Unit::Unit(const std::string& name)
    : m_name(name)
    {
        registerUnit( this );
    }

    void Unit::registerTest(TestFunc foo, const std::string& n)
    {
        TestCase testCase;
        testCase.foo = foo;
        testCase.name = n;

        m_testCases.push_back( testCase );
    }

    /**
    * Returns 0 if the the unit passed, 1 otherwise.
    */
    unsigned Unit::runTests()
    {
        unsigned fail = 0;
        std::cout << "Unit test: " << m_name << "\n";
        for(unsigned i=0; i<m_testCases.size(); ++i)
        {
            try
            {
                m_testCases[i].foo();
                std::cout << "  Test case '" << m_testCases[i].name << "' OK\n";
            }
            catch(const TestFailed& ex)
            {
                std::cout << "  Test case '" << m_testCases[i].name << "' FAILED!\n    "
                          << ex.what() << "\n";
                fail = 1;
            }
            catch(const std::exception& ex)
            {
                std::cout << "  Test case '" << m_testCases[i].name
                          << "' Unexpected exception!\n    " << ex.what() << "\n";
                fail = 1;
            }
            catch(...)
            {
                std::cout << "  Test case '" << m_testCases[i].name
                          << "' Unexpected type thrown!\n";
                fail = 1;
            }
        }

        return fail;
    }

    /**
    * Test if the expression is false, then report the expression, filename and line.
    */
    void assertOk(bool expr, const char* rep, const char* filename, unsigned line)
    {
        if(expr == false)
        {
            std::ostringstream msg;
            msg << filename << ":" << line << ": <" << rep << "> is false.";
            throw TestFailed( msg.str() );
        }
    }

    void handleMissedException(const std::string& type, const char* filename, unsigned line)
    {
        std::ostringstream msg;
        msg << filename << ":" << line
            <<  ": Expected exception " << type << ". Nothing caught.";
        throw tiny::TestFailed( msg.str() );
    }

    /**
    * Loop through all registered units and run their test cases.
    */
    int runUnits()
    {
        unsigned failedUnits = 0;
        const unsigned numUnits = TINY().unitTests.size();
        for(unsigned i=0; i<numUnits; ++i)
        {
            failedUnits += TINY().unitTests[i]->runTests();
        }

        std::cout << (numUnits-failedUnits) << "/" << numUnits << " unit tests passed.\n";

        if(failedUnits == 0) return 0;

        return 1;
    }
}

/**
* Return 0 when no test failed, 1 otherwise
*/
int main(int argc, char** argv)
{
    return tiny::runUnits();
}
\$\endgroup\$
3
  • 4
    \$\begingroup\$ Hi, You might want to keep either c++11 or c++14 and not both, asking to convert c++11 to c++14 would be off topic. Only keep the version you are using. \$\endgroup\$
    – JaDogg
    Commented Nov 6, 2014 at 8:07
  • 1
    \$\begingroup\$ @JaDogg: Yes, you are right. I just want to go to the newest. So C++14 it is. \$\endgroup\$
    – aggsol
    Commented Nov 6, 2014 at 10:22
  • 3
    \$\begingroup\$ throw() is deprecated. Use noexcept \$\endgroup\$
    – glampert
    Commented Nov 6, 2014 at 13:06

1 Answer 1

7
\$\begingroup\$

A question is hard to answer when the correct answer is, that it is good as it is.

Indeed the typical unit test framework implementation uses the __FILE__ and the __LINE__ macros.

In Qt it is implemented a very similar way as you did:

#define QVERIFY(statement) \
do {\
    if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\
        return;\
} while (0)

In Boost Test it is a little more complex, but the concept is the same:

#define BOOST_TEST_PASSPOINT()                              \
::boost::unit_test::unit_test_log.set_checkpoint(           \
    BOOST_TEST_L(__FILE__),                                 \
    static_cast<std::size_t>(__LINE__) )                    \

#define BOOST_CHECK_IMPL( P, check_descr, TL, CT )                  \
do {                                                                \
    BOOST_TEST_PASSPOINT();                                         \
    BOOST_TEST_TOOL_IMPL( check_impl, P, check_descr, TL, CT ), 0 );\
} while( ::boost::test_tools::dummy_cond )                          \

#define BOOST_CHECK( P ) \
BOOST_CHECK_IMPL( (P), BOOST_TEST_STRINGIZE( P ), CHECK, CHECK_PRED )

No matter how hard I think about it, I always get to the conclusion, that if you want to do it without these macros you will need some kind of stack trace (like in C#). But as C++ does not support reflections using these macros is your only option.

The one thing I would change in your code is adding the do { ... } while (0) guard around your macro (here is why).

\$\endgroup\$
2
  • \$\begingroup\$ The the macro way is the way to go then. I the wake of C++11 and C++14 I was thinking there could be something new to use I am missing. The link you have given does not advocate against using do {] while(0). Please give some some extra reasoning or alternative. \$\endgroup\$
    – aggsol
    Commented Mar 2, 2015 at 9:08
  • 2
    \$\begingroup\$ The link actually supports the use of the do {} while(0). I meant to suggest for you to use it for the TINY_ASSERT_OK and the TINY_ASSERT_EQUAL macros. \$\endgroup\$ Commented Mar 2, 2015 at 10:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.