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.

asked Jan 21, 2017 at 7:27
\$\endgroup\$
1

1 Answer 1

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);
}
answered Jan 22, 2017 at 2:57
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.