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.
-
\$\begingroup\$ I did something similar a while ago codereview.stackexchange.com/questions/69061/… \$\endgroup\$aggsol– aggsol2017年01月23日 10:22:49 +00:00Commented Jan 23, 2017 at 10:22
1 Answer 1
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);
}
Explore related questions
See similar questions with these tags.