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();
}
throw()
is deprecated. Usenoexcept
\$\endgroup\$