3
\$\begingroup\$

Here is a single-header small unit testing framework I wrote, that makes use of mostly preprocessor macros. I made it for knowing how unit testing works and, also, try to depend less of third-party libraries.

Code:

utest.hpp:

#ifndef CYNODELIC_UTEST_UTEST_HPP
#define CYNODELIC_UTEST_UTEST_HPP

/* Usage:
 * 1. Initialize the test suite
 * 2. Add tests
 * 3. Initialize the running function
 * 4. Run
 */

#include <iostream>
#include <cstdlib>
#include <cstdint>
#include <cmath>
#include <functional>
#include <type_traits>
#include <limits>
#include <string>
#include <vector>

// Print file and line info (do nothing if CYNODELIC_UTEST_DO_NOT_PRINT_LINE_INFO was defined)
#ifndef CYNODELIC_UTEST_DO_NOT_PRINT_LINE_INFO
    #define CYNODELIC_UTEST_FILE_LINE_OUT std::cout << "         File: " << __FILE__ << " on line " << __LINE__ << "\n";
#else
    #define CYNODELIC_UTEST_FILE_LINE_OUT
#endif

// Initialize test suite
#define CYNODELIC_UTEST_INITIALIZE_TEST_SUITE(name) \
namespace utest_suite_##name##_ \
{                               \
    std::vector<cynodelic::utest::test_cont>   test_list; \
    std::vector<cynodelic::utest::test_status> stats;     \
}                               \

// Checking
#define CYNODELIC_UTEST_CHECK_TRUE(cnd) \
do \
{  \
    if (!(cnd))                             \
    {                                       \
        std::cout << cynodelic::utest::internal::co_red("[FAILED] ")   << #cnd << "\n"; \
        CYNODELIC_UTEST_FILE_LINE_OUT;                                                  \
        stats.push_back(cynodelic::utest::test_status::FAIL);                           \
    }    \
    else \
    {    \
        std::cout << cynodelic::utest::internal::co_green("[ PASS ] ") << #cnd << "\n"; \
        stats.push_back(cynodelic::utest::test_status::PASS);                           \
    }    \
} while (false) \

#define CYNODELIC_UTEST_CHECK_FALSE(cnd) CYNODELIC_UTEST_CHECK_TRUE(!(cnd))

#define CYNODELIC_UTEST_CHECK_OPERATOR(op,a,b) \
do \
{  \
    if (!((a) op (b))) \
    {                  \
        std::cout << cynodelic::utest::internal::co_red("[FAILED] ")   << #a << " " << #op << " " << #b << "\n"; \
        CYNODELIC_UTEST_FILE_LINE_OUT;                                                                           \
        stats.push_back(cynodelic::utest::test_status::FAIL);                                                    \
    }    \
    else \
    {    \
        std::cout << cynodelic::utest::internal::co_green("[ PASS ] ") << #a << " " << #op << " " << #b << "\n"; \
        stats.push_back(cynodelic::utest::test_status::PASS);                                                    \
    }    \
} while (false) \

#define CYNODELIC_UTEST_CHECK_EQUALS(val_a,val_b)       CYNODELIC_UTEST_CHECK_OPERATOR(==,val_a,val_b)
#define CYNODELIC_UTEST_CHECK_NON_EQUALS(val_a,val_b)   CYNODELIC_UTEST_CHECK_OPERATOR(!=,val_a,val_b)
#define CYNODELIC_UTEST_CHECK_GREATER_THAN(val_a,val_b) CYNODELIC_UTEST_CHECK_OPERATOR(>,val_a,val_b)
#define CYNODELIC_UTEST_CHECK_LESSER_THAN(val_a,val_b)  CYNODELIC_UTEST_CHECK_OPERATOR(<,val_a,val_b)

#define CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(a,b)       \
do \
{  \
    if (!(cynodelic::utest::internal::almost_equals(a,b))) \
    {                                                      \
        std::cout << cynodelic::utest::internal::co_red("[FAILED] ")   << #a << " == " << #b << "\n"; \
        CYNODELIC_UTEST_FILE_LINE_OUT;                                                                \
        stats.push_back(cynodelic::utest::test_status::FAIL);                                         \
    }    \
    else \
    {    \
        std::cout << cynodelic::utest::internal::co_green("[ PASS ] ") << #a << " == " << #b << "\n"; \
        stats.push_back(cynodelic::utest::test_status::PASS);                                         \
    }    \
} while (false) \

// Assertion
#define CYNODELIC_UTEST_ASSERT_TRUE(cnd) \
do \
{  \
    if (!(cnd))                              \
    {                                        \
        std::cerr << cynodelic::utest::internal::co_red("[FAILED] ")   << #cnd << "\n"; \
        CYNODELIC_UTEST_FILE_LINE_OUT;                                                  \
        std::cout << "\n" << stats.size() << " tests have been passed.\n";              \
        std::exit(EXIT_FAILURE);                                                        \
    }    \
    else \
    {    \
        std::cout << cynodelic::utest::internal::co_green("[ PASS ] ") << #cnd << "\n"; \
        stats.push_back(cynodelic::utest::test_status::PASS);                           \
    }    \
} while (false) \

#define CYNODELIC_UTEST_ASSERT_FALSE(cnd) CYNODELIC_UTEST_ASSERT_TRUE(!(cnd))

#define CYNODELIC_UTEST_ASSERT_OPERATOR(op,a,b)                                      \
do \
{  \
    if (!((a) op (b)))                                                                   \
    {                                                                                    \
        std::cerr << cynodelic::utest::internal::co_red("[FAILED] ")   << #a << " " << #op << " " << #b << "\n"; \
        CYNODELIC_UTEST_FILE_LINE_OUT;                                                                           \
        std::cout << "\n" << stats.size() << " tests have been passed.\n";                                       \
        std::exit(EXIT_FAILURE);                                                                                 \
    }    \
    else \
    {    \
        std::cout << cynodelic::utest::internal::co_green("[ PASS ] ") << #a << " " << #op << " " << #b << "\n"; \
        stats.push_back(cynodelic::utest::test_status::PASS);                                                    \
    }    \
} while (false) \

#define CYNODELIC_UTEST_ASSERT_EQUALS(val_a,val_b)       CYNODELIC_UTEST_ASSERT_OPERATOR(==,val_a,val_b)
#define CYNODELIC_UTEST_ASSERT_NON_EQUALS(val_a,val_b)   CYNODELIC_UTEST_ASSERT_OPERATOR(!=,val_a,val_b)
#define CYNODELIC_UTEST_ASSERT_GREATER_THAN(val_a,val_b) CYNODELIC_UTEST_ASSERT_OPERATOR(>,val_a,val_b)
#define CYNODELIC_UTEST_ASSERT_LESSER_THAN(val_a,val_b)  CYNODELIC_UTEST_ASSERT_OPERATOR(<,val_a,val_b)

#define CYNODELIC_UTEST_ASSERT_FLOAT_EQUALS(a,b)      \
do \
{  \
    if (!(cynodelic::utest::internal::almost_equals(a,b))) \
    {                                                      \
        std::cerr << cynodelic::utest::internal::co_red("[FAILED] ")   << #a << " == " << #b << "\n"; \
        CYNODELIC_UTEST_FILE_LINE_OUT;                                                                \
        std::cout << "\n" << stats.size() << " tests have been passed.\n";                            \
        std::exit(EXIT_FAILURE);                                                                      \
    }    \
    else \
    {    \
        std::cout << cynodelic::utest::internal::co_green("[ PASS ] ") << #a << " == " << #b << "\n"; \
        stats.push_back(cynodelic::utest::test_status::PASS);                                         \
    }    \
} while (false) \

// Add a test
#define CYNODELIC_UTEST_ADD_TEST(name,method) \
namespace utest_suite_##name##_               \
{                                             \
    struct utest_##name##_method_##method##_holder \
    {  \
        utest_##name##_method_##method##_holder() \
        { \
            test_list.push_back(cynodelic::utest::test_cont( #name , #method ,&test_fun)); \
        } \
        static void test_fun(); \
    }; \
}          \
utest_suite_##name##_::utest_##name##_method_##method##_holder utest_##name##_method_##method; \
void utest_suite_##name##_::utest_##name##_method_##method##_holder::test_fun()                \

// Initialize the running function
#define CYNODELIC_UTEST_INITIALIZE_RUNNING(name) \
namespace utest_suite_##name##_                  \
{                                                \
    bool run_tests() \
    {                \
        std::cout << "==================== Test suite: " << #name << " ====================\n\n"; \
                                                     \
        for (std::size_t i=0;i<test_list.size();i++) \
        {                                            \
            std::cout << "Running " << test_list[i].test_method              \
                      << " test from " << test_list[i].test_name << "...\n"; \
            test_list[i]();                                                  \
            std::cout << "\n";                                               \
        }                                        \
                                                 \
        std::size_t passed_tests = 0;            \
        for (std::size_t i=0;i<stats.size();i++) \
            if (stats[i] == cynodelic::utest::test_status::PASS) \
                passed_tests++;                              \
                                                 \
        std::cout << passed_tests << " out of " << stats.size() << " tests have been passed.\n\n"; \
                                                 \
        if (passed_tests != stats.size()) \
            return false;             \
        return true; \
    } \
} \

// Run the tests
#define CYNODELIC_UTEST_RUN_TEST_SUITE(name) utest_suite_##name##_::run_tests()

namespace cynodelic { namespace utest {

namespace internal
{

// Check equality with double
template <typename FloatTypeA,typename FloatTypeB>
inline typename std::enable_if<
        std::is_arithmetic<FloatTypeA>::value && std::is_arithmetic<FloatTypeB>::value,
        bool
    >::type almost_equals(const FloatTypeA& a,const FloatTypeB& b)
{
    double a_ = static_cast<double>(a);
    double b_ = static_cast<double>(b);
    return std::fabs(a_-b_) < std::numeric_limits<double>::epsilon() || std::fabs(a_-b_) < std::numeric_limits<double>::min();
}

// Colorize output (if CYNODELIC_UTEST_NON_COLORIZED_OUTPUT is not defined)
#ifndef CYNODELIC_UTEST_NON_COLORIZED_OUTPUT
inline std::string co_red(const char *x)   { return std::string("\033[1;31m")+std::string(x)+std::string("\033[0m"); }
inline std::string co_green(const char *x) { return std::string("\033[1;32m")+std::string(x)+std::string("\033[0m"); }
#else
inline std::string co_red(const char *x)   { return std::string(x); }
inline std::string co_green(const char *x) { return std::string(x); }
#endif

} // end of "internal" namespace

// Test status enum (this is used instead of bool)
enum class test_status : std::uint8_t
{
    FAIL = 0xe0,
    PASS = 0xe1
};

// Test container
struct test_cont
{
    std::string test_name;      // Test name
    std::string test_method;    // Test method name
    std::function<void()> tmet; // The test method

    test_cont(const char *name,const char *method,void(*tm_ptr)()) :
        test_name(name),
        test_method(method),
        tmet(tm_ptr) {}

    // Run the test contained
    void operator()()
    {
        tmet();
    }
};

}} // end of "cynodelic::utest" namespace

#endif

utest_demo.cpp:

#include <iostream>
#include <cstdlib>
#include <utility>

#include "utest.hpp"

// This class was made for demonstration
class vec3
{
    public:
        double x, y, z;
        vec3() : x(0), y(0), z(0) {}
        vec3(double x,double y,double z) : x(x), y(y), z(z) {}

        vec3(const vec3& other) : x(other.x), y(other.y), z(other.z) {}

        vec3& operator=(const vec3& other)
        {
            x = other.x;
            y = other.y;
            z = other.z;
            return *this;
        }

        void swap(vec3& other)
        {
            std::swap(x,other.x);
            std::swap(y,other.y);
            std::swap(z,other.z);
        }

        friend void swap(vec3& lhs,vec3& rhs)
        {
            lhs.swap(rhs);
        }

        friend bool operator==(const vec3& lhs,const vec3& rhs)
        {
            return (lhs.x == rhs.x)&&(lhs.y == rhs.y)&&(lhs.z == rhs.z);
        }

        friend bool operator!=(const vec3& lhs,const vec3& rhs)
        {
            return !(lhs == rhs);
        }

        vec3& operator+=(const vec3& other)
        {
            x += other.x;
            y += other.y;
            z += other.z;
            return *this;
        }

        friend vec3 operator+(vec3 lhs,const vec3& rhs)
        {
            lhs += rhs;
            return lhs;
        }

        vec3& operator-=(const vec3& other)
        {
            x -= other.x;
            y -= other.y;
            z -= other.z;
            return *this;
        }

        friend vec3 operator-(vec3 lhs,const vec3& rhs)
        {
            lhs -= rhs;
            return lhs;
        }
};

CYNODELIC_UTEST_INITIALIZE_TEST_SUITE(vec3);

CYNODELIC_UTEST_ADD_TEST(vec3,copy_construction)
{
    vec3 t1(0.5,0.25,0.125);
    vec3 t2(t1);
    CYNODELIC_UTEST_CHECK_EQUALS(t1,t2);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.x,t2.x);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.y,t2.y);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.z,t2.z);
}

CYNODELIC_UTEST_ADD_TEST(vec3,assignment)
{
    vec3 t1(1,2,3);
    vec3 t2(0.1,0,-0.1);
    t1 = t2;
    CYNODELIC_UTEST_CHECK_EQUALS(t1,t2);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.x,t2.x);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.y,t2.y);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.z,t2.z);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.x,0.1);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.y,0);
    CYNODELIC_UTEST_CHECK_EQUALS(t1.z,-0.1);
}

CYNODELIC_UTEST_ADD_TEST(vec3,equality)
{
    vec3 v1(1.111,2.345,3.0005);
    vec3 v2(1.111,2.345,3.0005);
    CYNODELIC_UTEST_CHECK_TRUE(v1 == v2);
    CYNODELIC_UTEST_CHECK_TRUE(v1.x == v2.x);
    CYNODELIC_UTEST_CHECK_TRUE(v1.y == v2.y);
    CYNODELIC_UTEST_CHECK_TRUE(v1.z == v2.z);
    CYNODELIC_UTEST_CHECK_NON_EQUALS(v1,v2);
}

CYNODELIC_UTEST_ADD_TEST(vec3,inequality)
{
    vec3 v1(0.01,0.1,1);
    vec3 v2(2,4,8);
    CYNODELIC_UTEST_CHECK_TRUE(v1 != v2);
    CYNODELIC_UTEST_CHECK_TRUE(v1.x != v2.x);
    CYNODELIC_UTEST_CHECK_TRUE(v1.y != v2.y);
    CYNODELIC_UTEST_CHECK_TRUE(v1.z != v2.z);
}

CYNODELIC_UTEST_ADD_TEST(vec3,swapping)
{
    vec3 v1(-1.1,-1.2,2.1);
    vec3 v2(14.32,23.14,32.41);
    swap(v1,v2);
    CYNODELIC_UTEST_CHECK_EQUALS(v1.x,14.32);
    CYNODELIC_UTEST_CHECK_EQUALS(v1.y,23.14);
    CYNODELIC_UTEST_CHECK_EQUALS(v1.z,32.41);
    CYNODELIC_UTEST_CHECK_EQUALS(v2.x,-1.1);
    CYNODELIC_UTEST_CHECK_EQUALS(v2.y,-1.2);
    CYNODELIC_UTEST_CHECK_EQUALS(v2.z,2.1);
    CYNODELIC_UTEST_CHECK_NON_EQUALS(v1,v2);
    CYNODELIC_UTEST_CHECK_NON_EQUALS(v1.x,v2.x);
    CYNODELIC_UTEST_CHECK_NON_EQUALS(v1.y,v2.y);
    CYNODELIC_UTEST_CHECK_NON_EQUALS(v1.z,v2.z);
}

CYNODELIC_UTEST_ADD_TEST(vec3,addition)
{
    vec3 v1(1,2,3);
    vec3 v2(5,6,7);
    vec3 res = v1+v2;
    CYNODELIC_UTEST_CHECK_EQUALS(v1+v2,res);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.x+v2.x,res.x);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.y+v2.y,res.y);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.z+v2.z,res.z);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.x+v2.x,6);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.y+v2.y,8);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.z+v2.z,10);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(res.x,6);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(res.y,8);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(res.z,10);
}

CYNODELIC_UTEST_ADD_TEST(vec3,substraction)
{
    vec3 v1(1.25,0.75,4.101001);
    vec3 v2(0.5 ,2.25,4);
    vec3 res = v1-v2;
    CYNODELIC_UTEST_CHECK_EQUALS(v1-v2,res);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.x-v2.x,res.x);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.y-v2.y,res.y);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.z-v2.z,res.z);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.x-v2.x,0.75);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.y-v2.y,-1.5);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(v1.z-v2.z,0.101001);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(res.x,0.75);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(res.y,-1.5);
    CYNODELIC_UTEST_CHECK_FLOAT_EQUALS(res.z,0.101001);
}

// Initialize the test suite running function
CYNODELIC_UTEST_INITIALIZE_RUNNING(vec3);

int main(int argc,char **argv)
{
    // Check if all the tests passed or failed
    bool test_res = CYNODELIC_UTEST_RUN_TEST_SUITE(vec3);

    if (!test_res)
        std::cout << "Test has been failed.\n";
    else
        std::cout << "Test finished successfully.\n";
}

Any suggestions, improvements, etc. are welcome, especially regarding its design.

\$\endgroup\$
1
\$\begingroup\$

Unit tests are great (as is testing).

When one of the test indicates a failure in the test it may take a while to find the actual problem. As a result you may want to re-run a specific test (or set of tests) without running all the tests (as all the test could take 30 minutes).

As a result I would expect to be able to pass it run time arguments on the command line to specify which tests I want to run (if no arguments are passed then run all the tests).

If you run unit tests in isolation you need a way to make sure that the environment is setup correctly (as some tests may change the global state). As a result most testing frameworks provide a SetUp and TearDown methods on each class. These are called before and after (respectively) each test.

Why does CYNODELIC_UTEST_RUN_TEST_SUITE() need to know the name of the test. This should run all test suites. Otherwise you open yourself to accidently leaving out test suites.

Personally I would pass the command line arguments to this macro.

int main(int argc, char* argv[])
{
    CYNODELIC_UTEST_RUN_TEST_SUITE(argc, argv);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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