├── .gitignore ├── CMakeLists.txt ├── README.md ├── conanfile.py ├── examples ├── CMakeLists.txt ├── libexample │ ├── CMakeLists.txt │ ├── example.cpp │ └── include │ │ └── libexample │ │ └── example.hpp ├── python_equivalent │ ├── mynamespace.py │ └── test_example.py └── test_example.hpp ├── include └── unittest │ ├── begin_main.hpp │ ├── detail │ ├── exceptions.hpp │ └── testrunner.hpp │ ├── end_main.hpp │ └── unittest.hpp ├── main.cpp.in └── unittest.cmake /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.tinyrefl 3 | *.pyc 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project(unittest) 3 | 4 | if(CMAKE_CURRENT_SOURCE_DIR EQUAL CMAKE_SOURCE_DIR) 5 | set(UNITTEST_IS_MAIN_PROJECT TRUE) 6 | else() 7 | set(UNITTEST_IS_MAIN_PROJECT TRUE) 8 | endif() 9 | 10 | set(CONANBUILDINFO_FILE "${CMAKE_CURRENT_BINARY_DIR}/conanbuildinfo.cmake") 11 | 12 | if(NOT EXISTS "${CONANBUILDINFO_FILE}") 13 | message(FATAL_ERROR "conanbuildinfo.cmake file (${CONANBUILDINFO_FILE}) not found. Please run conan install ${CMAKE_CURRENT_SOURCE_DIR} --build=missing from your build directory first") 14 | else() 15 | include("${CONANBUILDINFO_FILE}") 16 | conan_basic_setup(TARGETS) 17 | endif() 18 | 19 | set(CMAKE_CXX_STANDARD 17) 20 | set(CMAKE_CXX_STANDARD_REQUIRED TRUE) 21 | 22 | set(UNITTEST_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include") 23 | 24 | add_library(unittest INTERFACE) 25 | target_include_directories(unittest INTERFACE "${UNITTEST_INCLUDE_DIR}") 26 | target_link_libraries(unittest INTERFACE 27 | CONAN_PKG::tinyrefl 28 | CONAN_PKG::ctti 29 | CONAN_PKG::CTRE 30 | CONAN_PKG::fmt 31 | CONAN_PKG::backward 32 | CONAN_PKG::elfspy 33 | dw 34 | ) 35 | target_compile_definitions(unittest INTERFACE 36 | BACKWARD_HAS_UNWIND=1 37 | BACKWARD_HAS_BACKTRACE=0 38 | BACKWARD_HAS_BACKTRACE_SYMBOL=0 39 | BACKWARD_HAS_DW=1 40 | BACKWARD_HAS_BFD=0 41 | ) 42 | 43 | option(UNITTESTS_BUILD_EXAMPLES "Build unittest examples" ${UNITTEST_IS_MAIN_PROJECT}) 44 | 45 | if(UNITTESTS_BUILD_EXAMPLES) 46 | include(unittest.cmake) 47 | add_subdirectory(examples) 48 | endif() 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unittest 2 | 3 | C++ unit testing and mocking made easy 4 | 5 | ## What? 6 | 7 | `unittest` is a proof of concept C++ unit testing framework inspired by 8 | Python's [`unittest` 9 | package](https://docs.python.org/3/library/unittest.html). 10 | 11 | `unitest` is designed with the following goals in mind: 12 | 13 | 1. **Zero plumbing code**: No setup functions, no main, no test 14 | registration. The library uses static reflection to figure out where 15 | are the tests declared, what is and what's not test case code, etc. 16 | 17 | 2. **Zero macros**: Every line of user code is just perfectly normal C++ 18 | code. 19 | 20 | 3. **Non intrusive arbitrary function and class mocking**: No need to use 21 | virtual function interfaces, mock classes, and dependency injection, 22 | which couple your library design with the way the mocking framework 23 | works. `unittest` uses monkey-patching through 24 | [`elfspy`](https://github.com/Manu343726/elfspy) so mocking is as 25 | transparent as possible. 26 | 27 | 4. **Expressive test failure output**: `unittest` not only mimics Python's 28 | `unittest` API but also its console output, including descriptive 29 | assertion error details, call arguments, location of the failed 30 | assertion in the code, etc. 31 | 32 | 33 | 5. **Easy integration**: Just pull the library from [conan](https://conan.io/) 34 | and use the provided `add_unittest()` CMake function to add a unittest 35 | executable to your project: 36 | 37 | ``` cmake 38 | add_unittest( 39 | NAME 40 | test_example 41 | TESTS 42 | test_example.hpp 43 | DEPENDENCIES 44 | libexample 45 | ) 46 | ``` 47 | 48 | Here is a full example of a unit test case with `unittest`: 49 | 50 | ``` cpp 51 | #include 52 | #include 53 | #include 54 | 55 | namespace test_example 56 | { 57 | 58 | struct ExampleTestCase : public unittest::TestCase 59 | { 60 | [[unittest::patch("mynamespace::ExampleClass::identity(int) const", return_value=42)]] 61 | void test_another_one_bites_the_dust(unittest::MethodSpy& identity) 62 | { 63 | mynamespace::ExampleClass object; 64 | 65 | self.assertEqual(object.methodThatCallsIdentity(), 42); 66 | identity.assert_called_once_with(43); 67 | } 68 | }; 69 | 70 | } 71 | ``` 72 | 73 | ``` shell 74 | test_another_one_bites_the_dust (test_example::ExampleTestCase) ... FAIL 75 | 76 | ======================================================================= 77 | FAIL: test_another_one_bites_the_dust (test_example::ExampleTestCase) 78 | ----------------------------------------------------------------------- 79 | Stack trace (most recent call last): 80 | #0 Source "/home/manu343726/Documentos/unittest/examples/test_example.hpp", line 16, in test_another_one_b 81 | ites_the_dust 82 | 13: mynamespace::ExampleClass object; 83 | 14: 84 | 15: self.assertEqual(object.methodThatCallsIdentity(), 42); 85 | > 16: identity.assert_called_once_with(43); 86 | 17: } 87 | 18: }; 88 | 89 | AssertionError: Expected call: mynamespace::ExampleClass::identity(43) 90 | Actual call: mynamespace::ExampleClass::identity(42) 91 | 92 | ----------------------------------------------------------------------- 93 | Ran 1 tests in 0.002s 94 | 95 | FAILED (failures=1) 96 | ``` 97 | 98 | Who said C++ could not be as expressive as Python? 99 | 100 | ``` python 101 | import unittest, unittest.mock 102 | import mynamespace 103 | 104 | class ExampleTestCase(unittest.TestCase): 105 | 106 | @unittest.mock.patch('mynamespace.ExampleClass.identity', return_value=42) 107 | def test_another_one_bites_the_dust(self, identity): 108 | object = mynamespace.ExampleClass() 109 | 110 | self.assertEqual(object.methodThatCallsIdentity(), 42) 111 | identity.assert_called_once_with(43) 112 | ``` 113 | 114 | ``` shell 115 | test_another_one_bites_the_dust (test_example.ExampleTestCase) ... FAIL 116 | 117 | ====================================================================== 118 | FAIL: test_another_one_bites_the_dust (test_example.ExampleTestCase) 119 | ---------------------------------------------------------------------- 120 | Traceback (most recent call last): 121 | File "/usr/lib/python3.7/unittest/mock.py", line 1195, in patched 122 | return func(*args, **keywargs) 123 | File "/home/manu343726/Documentos/unittest/examples/python_equivalent/test_example.py", line 11, in test_another_one_bites_ 124 | the_dust 125 | identity.assert_called_once_with(43) 126 | File "/usr/lib/python3.7/unittest/mock.py", line 831, in assert_called_once_with 127 | return self.assert_called_with(*args, **kwargs) 128 | File "/usr/lib/python3.7/unittest/mock.py", line 820, in assert_called_with 129 | raise AssertionError(_error_message()) from cause 130 | AssertionError: Expected call: identity(43) 131 | Actual call: identity(42) 132 | 133 | ---------------------------------------------------------------------- 134 | Ran 1 test in 0.002s 135 | 136 | FAILED (failures=1) 137 | ``` 138 | 139 | ## Requirements 140 | 141 | - Compiler with C++ 17 support 142 | - Conan with my [bintray repository](https://bintray.com/beta/#/manu343726/conan-packages?tab=packages) configured. 143 | - CMake >= 3.0 144 | - Linux x86_64 (See [elfspy requirements](https://github.com/mollismerx/elfspy/wiki/Dependencies) for details). 145 | - The code must be built with optimizations disabled and full debug information 146 | (`-O0 -g3`). 147 | - libelf, libdwarf, or any other debug info reading library (See [backward-cpp 148 | install instructions](https://github.com/bombela/backward-cpp#libraries-to-read-the-debug-info) for details). 149 | 150 | ## Running the examples 151 | 152 | ``` shell 153 | $ git clone https://github.com/Manu343726/unittest && cd unittest 154 | $ mkdir build && cd build 155 | $ conan install .. --build=missing 156 | $ cmake .. -DCMAKE_BUILD_TYPE=Debug 157 | $ make 158 | $ ctest . -V 159 | ``` 160 | 161 | ## Roadmap 162 | 163 | - [ ] Mocking full classes with one patch call 164 | - [ ] Multiple patch calls in one test 165 | - [ ] Namespaces with free functions as test cases 166 | - [ ] Mocking of private methods 167 | - [ ] `patch("method", return_value=foo)` syntax 168 | 169 | ## Documentation 170 | 171 | Yeah, sorry, this is a work in progress PoC. The code is given as ugly as it 172 | looks without any comment or docstring that could make things clear. 173 | 174 | ## License 175 | 176 | Everything is released under MIT license. 177 | 178 | ## Production ready 179 | 180 | Yeah, sure. 181 | -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | from conans import python_requires 2 | 3 | common = python_requires('conan_common_recipes/0.0.8@Manu343726/testing') 4 | 5 | class UnittestConan(common.CMakePackage): 6 | name = 'unittest' 7 | version = '0.0.0' 8 | license = 'MIT' 9 | requires = ('tinyrefl/0.4.1@Manu343726/testing', 10 | 'ctti/0.0.2@Manu343726/testing', 11 | 'fmt/5.3.0@bincrafters/stable', 12 | 'CTRE/v2.4@ctre/stable', 13 | 'backward/1.3.1@Manu343726/stable', 14 | 'elfspy/master@Manu343726/testing') 15 | build_requires = 'tinyrefl-tool/0.4.1@Manu343726/testing' 16 | generators = 'cmake' 17 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(libexample) 2 | 3 | add_unittest( 4 | NAME 5 | test_example 6 | TESTS 7 | test_example.hpp 8 | DEPENDENCIES 9 | libexample 10 | ) 11 | -------------------------------------------------------------------------------- /examples/libexample/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(libexample SHARED example.cpp) 2 | target_include_directories(libexample PUBLIC "include/") 3 | 4 | find_package(tinyrefl_tool REQUIRED) 5 | tinyrefl_tool(TARGET libexample HEADERS include/libexample/example.hpp) 6 | -------------------------------------------------------------------------------- /examples/libexample/example.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace mynamespace; 4 | 5 | int ExampleClass::identity(int input) const 6 | { 7 | return input; 8 | } 9 | 10 | int ExampleClass::methodThatCallsIdentity() const 11 | { 12 | return identity(42); 13 | } 14 | -------------------------------------------------------------------------------- /examples/libexample/include/libexample/example.hpp: -------------------------------------------------------------------------------- 1 | namespace mynamespace 2 | { 3 | class ExampleClass 4 | { 5 | public: 6 | int identity(int input) const; 7 | int methodThatCallsIdentity() const; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /examples/python_equivalent/mynamespace.py: -------------------------------------------------------------------------------- 1 | 2 | class ExampleClass: 3 | 4 | def identity(self, value): 5 | return value 6 | 7 | def methodThatCallsIdentity(self): 8 | return self.identity(42) 9 | -------------------------------------------------------------------------------- /examples/python_equivalent/test_example.py: -------------------------------------------------------------------------------- 1 | import unittest, unittest.mock 2 | import mynamespace 3 | 4 | class ExampleTestCase(unittest.TestCase): 5 | 6 | @unittest.mock.patch('mynamespace.ExampleClass.identity', return_value=42) 7 | def test_another_one_bites_the_dust(self, identity): 8 | object = mynamespace.ExampleClass() 9 | 10 | self.assertEqual(object.methodThatCallsIdentity(), 42) 11 | identity.assert_called_once_with(43) 12 | -------------------------------------------------------------------------------- /examples/test_example.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace test_example 6 | { 7 | 8 | struct ExampleTestCase : public unittest::TestCase 9 | { 10 | [[unittest::patch("mynamespace::ExampleClass::identity(int) const")]] 11 | void test_another_one_bites_the_dust(unittest::MethodSpy& identity) 12 | { 13 | mynamespace::ExampleClass object; 14 | 15 | self.assertEqual(object.methodThatCallsIdentity(), 42); 16 | identity.assert_called_once_with(43); 17 | } 18 | }; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /include/unittest/begin_main.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UNITTEST_BEGIN_MAIN_HPP_INCLUDED 2 | #define UNITTEST_BEGIN_MAIN_HPP_INCLUDED 3 | 4 | #include 5 | 6 | #endif // UNITTEST_BEGIN_MAIN_HPP_INCLUDED 7 | -------------------------------------------------------------------------------- /include/unittest/detail/exceptions.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UNITTEST_DETAIL_EXCEPTIONS_HPP 2 | #define UNITTEST_DETAIL_EXCEPTIONS_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace unittest 9 | { 10 | 11 | namespace detail 12 | { 13 | 14 | class TestAssertionException final : public std::exception 15 | { 16 | public: 17 | TestAssertionException(std::string what, backward::StackTrace backtrace) : 18 | _what{std::move(what)}, 19 | backtrace{std::move(backtrace)} 20 | {} 21 | 22 | const char* what() const noexcept override 23 | { 24 | return _what.c_str(); 25 | } 26 | 27 | backward::StackTrace backtrace; 28 | 29 | private: 30 | std::string _what; 31 | }; 32 | 33 | } // namespace unittest::detail 34 | 35 | } // namespace unittest 36 | 37 | #endif // UNITTEST_DETAIL_EXCEPTIONS_HPP 38 | -------------------------------------------------------------------------------- /include/unittest/detail/testrunner.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UNITTEST_DETAIL_TESTRUNNER_HPP 2 | #define UNITTEST_DETAIL_TESTRUNNER_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | FMT_BEGIN_NAMESPACE 20 | 21 | namespace internal 22 | { 23 | 24 | template<> 25 | struct is_like_std_string : std::true_type {}; 26 | 27 | } 28 | 29 | FMT_END_NAMESPACE 30 | 31 | namespace unittest 32 | { 33 | 34 | namespace detail 35 | { 36 | 37 | template 38 | constexpr bool is_test_method() 39 | { 40 | constexpr std::size_t prefix_length = sizeof("test_") - 1; 41 | return Method::name.name().size() >= prefix_length and Method::name.name()(0, prefix_length) == "test_"; 42 | } 43 | 44 | struct AssertionFailure 45 | { 46 | std::string methodName; 47 | std::string testCaseName; 48 | std::string message; 49 | std::optional backtrace; 50 | }; 51 | 52 | int print_failures(const std::vector& failures, const int totalTestsRun, const std::chrono::milliseconds& elapsedTime) 53 | { 54 | std::size_t longest_separator_length = 0; 55 | 56 | for(const auto& failure : failures) 57 | { 58 | const auto failure_title = fmt::format("FAIL: {} ({})\n", failure.methodName, failure.testCaseName); 59 | const std::string hard_separator(failure_title.size() + 1, '='); 60 | const std::string soft_separator(failure_title.size() + 1, '-'); 61 | fmt::print(stderr, "\n{}\n", hard_separator); 62 | fmt::print(stderr, failure_title); 63 | fmt::print(stderr, "{}\n", soft_separator); 64 | 65 | if(failure.backtrace) 66 | { 67 | backward::Printer backtracePrinter; 68 | backtracePrinter.print(failure.backtrace.value(), stderr); 69 | } 70 | 71 | fmt::print(stderr, "\n{}\n", failure.message); 72 | 73 | longest_separator_length = std::max(longest_separator_length, soft_separator.size()); 74 | } 75 | 76 | fmt::print("\n{}\n", std::string(longest_separator_length, '-')); 77 | fmt::print("Ran {} tests in {: .3f}s\n", totalTestsRun, elapsedTime.count() / 1000.0f); 78 | 79 | if(failures.empty()) 80 | { 81 | fmt::print("\nOK\n"); 82 | return EXIT_SUCCESS; 83 | } 84 | else 85 | { 86 | fmt::print("\nFAILED (failures={})\n", failures.size()); 87 | return EXIT_FAILURE; 88 | } 89 | } 90 | 91 | template 92 | void dump_method_decorators(Method method) 93 | { 94 | for(const auto& attribute : method.get_attributes()) 95 | { 96 | if(attribute.namespace_.full_name() == "unittest") 97 | { 98 | fmt::print("{} has unittest decorator {}\n", Method::name.full_name(), attribute.full_attribute); 99 | } 100 | } 101 | } 102 | 103 | template 104 | struct is_testcase_metatype_impl : tinyrefl::meta::false_ {}; 105 | 106 | template 107 | struct is_testcase_metatype_impl : std::is_base_of 108 | {}; 109 | 110 | template 111 | struct is_testcase_metatype : is_testcase_metatype_impl {}; 112 | 113 | template 114 | struct MethodSignature; 115 | 116 | template 117 | struct MethodSignature 118 | { 119 | using return_type = R; 120 | using class_type = Class; 121 | using args = tinyrefl::meta::list; 122 | static constexpr bool is_const = false; 123 | }; 124 | 125 | template 126 | struct MethodSignature 127 | { 128 | using return_type = R; 129 | using class_type = Class; 130 | using args = tinyrefl::meta::list; 131 | static constexpr bool is_const = true; 132 | }; 133 | 134 | template 135 | using method_return_type = typename MethodSignature::return_type; 136 | template 137 | using method_class_type = typename MethodSignature::class_type; 138 | template 139 | using method_args_types = typename MethodSignature::args; 140 | 141 | template 142 | struct MethodSpyImpl; 143 | 144 | template 145 | struct MethodSpyImpl> 146 | : public spy::Hook>, R, Class*, Args...>, 147 | public MethodSpy 148 | { 149 | using This = MethodSpyImpl>; 150 | using Base = spy::Hook; 151 | 152 | template 153 | MethodSpyImpl(std::index_sequence) : 154 | Base{ 155 | MethodMetadata::full_display_name.begin(), 156 | spy::Method{MethodMetadata{}.get()}.resolve() 157 | }, 158 | _callsHandle{spy::call(*this)}, 159 | _argHandles{spy::arg(*this)...}, 160 | _resultHandle{spy::result(*this)} 161 | {} 162 | 163 | MethodSpyImpl() : 164 | MethodSpyImpl{std::index_sequence_for{}} 165 | {} 166 | 167 | using ArgsTuple = std::tuple...>; 168 | using ArgsTuples = std::vector; 169 | 170 | ArgsTuples call_args_list() override final 171 | { 172 | ArgsTuples result; 173 | 174 | for(std::size_t i = 0; i < spy::call(*this).count(); ++i) 175 | { 176 | result.emplace_back(call_args(i)); 177 | } 178 | 179 | return result; 180 | } 181 | 182 | void assert_called() 183 | { 184 | if(!called()) 185 | { 186 | backward::StackTrace st; st.load_here(4); 187 | st.skip_n_firsts(3); 188 | 189 | throw unittest::detail::TestAssertionException{ 190 | fmt::format("AssertionError: Expected '{}' to have been called.", MethodMetadata::full_display_name), 191 | std::move(st) 192 | }; 193 | } 194 | } 195 | 196 | void _assert_called_once() 197 | { 198 | if(_callsHandle.count() != 1) 199 | { 200 | backward::StackTrace st; st.load_here(5); 201 | st.skip_n_firsts(4); 202 | 203 | throw unittest::detail::TestAssertionException{ 204 | fmt::format("AssertionError: Expected '{}' to have been called once. Called {} times.", MethodMetadata::full_display_name, _callsHandle.count()), 205 | std::move(st) 206 | }; 207 | } 208 | } 209 | 210 | void assert_called_once() override final 211 | { 212 | _assert_called_once(); 213 | } 214 | 215 | void assert_called_once_with(Args... args) override final 216 | { 217 | _assert_called_once(); 218 | _assert_called_with(args...); 219 | } 220 | 221 | void _assert_called_with(Args... args) 222 | { 223 | if(!called()) 224 | { 225 | backward::StackTrace st; st.load_here(5); 226 | st.skip_n_firsts(4); 227 | 228 | throw unittest::detail::TestAssertionException{fmt::format( 229 | "AssertionError: Expected call: {}\nNot called", dump_call(args...)), 230 | std::move(st)}; 231 | } 232 | else 233 | { 234 | const auto last_call = last_call_args(); 235 | auto call_args = std::make_tuple(args...); 236 | 237 | if(call_args != last_call) 238 | { 239 | backward::StackTrace st; st.load_here(5); 240 | st.skip_n_firsts(4); 241 | 242 | throw unittest::detail::TestAssertionException{fmt::format( 243 | "AssertionError: Expected call: {}\nActual call: {}", dump_call(call_args), dump_call(last_call)), 244 | std::move(st)}; 245 | } 246 | } 247 | } 248 | 249 | void assert_called_with(Args... args) 250 | { 251 | _assert_called_with(args...); 252 | } 253 | 254 | private: 255 | template 256 | using ArgHandle = spy::ThunkHandle::template Type>; 257 | 258 | template 259 | struct ArgHandlesForIndices; 260 | 261 | template 262 | struct ArgHandlesForIndices> 263 | { 264 | using type = std::tuple...>; 265 | }; 266 | 267 | using ArgHandles = typename ArgHandlesForIndices>::type; 268 | using ResultHandle = spy::ThunkHandle>; 269 | using CallsHandle = spy::ThunkHandle; 270 | 271 | CallsHandle _callsHandle; 272 | ArgHandles _argHandles; 273 | ResultHandle _resultHandle; 274 | 275 | 276 | void assertFailure(const std::string& message) 277 | { 278 | } 279 | 280 | bool called() 281 | { 282 | return _callsHandle.count() > 0; 283 | } 284 | 285 | bool called_once() 286 | { 287 | return _callsHandle.count() == 1; 288 | } 289 | 290 | bool called_once_with(Args... args) 291 | { 292 | return assert_called_once() && call_has_args(0, args...); 293 | } 294 | 295 | bool called_with(Args... args) 296 | { 297 | for(std::size_t i = 0; i < _callsHandle.count(); ++i) 298 | { 299 | if(call_has_args(i, args...)) 300 | { 301 | return true; 302 | } 303 | } 304 | 305 | return false; 306 | } 307 | bool call_has_args(const std::size_t call_index, Args... args) 308 | { 309 | return call_args(call_index) == std::make_tuple(args...); 310 | } 311 | 312 | ArgsTuple call_args(const std::size_t call_index) 313 | { 314 | return call_args_impl(std::make_index_sequence{}, call_index); 315 | } 316 | 317 | ArgsTuple last_call_args() 318 | { 319 | return call_args(_callsHandle.count() - 1); 320 | } 321 | 322 | template 323 | ArgsTuple call_args_impl(std::index_sequence, const std::size_t call_index) 324 | { 325 | return std::make_tuple(std::get(_argHandles).value(call_index)...); 326 | } 327 | 328 | std::string dump_call_by_index(const std::size_t call_index) 329 | { 330 | return dump_call(call_args(call_index)); 331 | } 332 | 333 | std::string dump_call(Args... args) 334 | { 335 | return dump_call(std::make_tuple(args...)); 336 | } 337 | 338 | std::string dump_call(const ArgsTuple& args) 339 | { 340 | return fmt::format("{}{}", MethodMetadata::name.full_name(), args); 341 | } 342 | }; 343 | 344 | 345 | 346 | template 347 | struct MethodSpyInstance : public MethodSpyImpl< 348 | MethodMetadata, 349 | method_return_type, 350 | method_class_type, 351 | method_args_types 352 | > {}; 353 | 354 | template 355 | void runTestCase(int& total_tests, std::vector& failures) 356 | { 357 | static_assert(std::is_base_of_v && 358 | tinyrefl::has_metadata(), 359 | "Expected test case class, that is, a class inheriting from unittest::TestCase" 360 | " and static reflection metadata"); 361 | 362 | TestCase testCase; 363 | 364 | tinyrefl::visit_class([&](auto /* testName */, auto /* depth */, auto method, 365 | TINYREFL_STATIC_VALUE(tinyrefl::entity::MEMBER_FUNCTION)) -> std::enable_if_t< 366 | is_test_method()> 367 | { 368 | using Method = decltype(method); 369 | constexpr Method constexpr_method; 370 | 371 | fmt::print("{} ({}) ... ", Method::name.name(), tinyrefl::metadata::name.full_name()); 372 | 373 | try 374 | { 375 | if constexpr (constexpr_method.has_attribute("patch") && 376 | constexpr_method.get_attribute("patch").namespace_.full_name() == "unittest" && 377 | constexpr_method.get_attribute("patch").args.size() >= 1) 378 | { 379 | constexpr auto target_id = constexpr_method.get_attribute("patch").args[0].pad(1,1); 380 | 381 | if constexpr (tinyrefl::has_entity_metadata()) 382 | { 383 | using Target = tinyrefl::entity_metadata; 384 | 385 | MethodSpyInstance spy; 386 | method.get(testCase, spy); 387 | } 388 | else 389 | { 390 | static_assert(sizeof(TestCase) != sizeof(TestCase), 391 | "[[unittest::patch()]] target not found"); 392 | } 393 | } 394 | else 395 | { 396 | method.get(testCase); 397 | } 398 | fmt::print("ok\n"); 399 | }catch(const unittest::detail::TestAssertionException& ex) 400 | { 401 | fmt::print("FAIL\n"); 402 | 403 | failures.push_back({ 404 | Method::name.name().str(), 405 | tinyrefl::metadata::name.full_name().str(), 406 | ex.what(), 407 | std::make_optional(ex.backtrace) 408 | }); 409 | 410 | }catch(const std::exception& ex) 411 | { 412 | fmt::print("FAIL\n"); 413 | 414 | failures.push_back({ 415 | Method::name.name().str(), 416 | tinyrefl::metadata::name.full_name().str(), 417 | fmt::format("Unhandled exception thrown while running test case: {}", ex.what()), 418 | std::nullopt 419 | }); 420 | } 421 | 422 | total_tests++; 423 | }); 424 | } 425 | 426 | template 427 | int runTestCases() 428 | { 429 | int total_tests = 0; 430 | std::vector failures; 431 | const auto start = std::chrono::high_resolution_clock::now(); 432 | 433 | tinyrefl::meta::foreach([&](auto type, auto /* index */) 434 | { 435 | using TestCaseMetadata = typename decltype(type)::type; 436 | 437 | runTestCase(total_tests, failures); 438 | }); 439 | 440 | return print_failures(failures, total_tests, 441 | std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start)); 442 | } 443 | 444 | } // namespace unittest::detail 445 | 446 | } // namespace unittest 447 | 448 | #endif // UNITTEST_DETAIL_TESTRUNNER_HPP 449 | -------------------------------------------------------------------------------- /include/unittest/end_main.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UNITTEST_END_MAIN_HPP_INCLUDED 2 | #define UNITTEST_END_MAIN_HPP_INCLUDED 3 | 4 | #include 5 | 6 | using test_cases = tinyrefl::meta::filter_t< 7 | tinyrefl::meta::defer, 8 | tinyrefl::entities 9 | >; 10 | 11 | int main(int argc, char** argv) 12 | { 13 | spy::initialise(argc, argv); 14 | return unittest::detail::runTestCases(); 15 | } 16 | 17 | #endif // UNITTEST_END_MAIN_HPP_INCLUDED 18 | -------------------------------------------------------------------------------- /include/unittest/unittest.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UNITTEST_UNITTEST_HPP 2 | #define UNITTEST_UNITTEST_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | inline std::any return_value; 10 | 11 | namespace unittest 12 | { 13 | 14 | template 15 | struct MethodSpy; 16 | 17 | template 18 | struct MethodSpy 19 | { 20 | virtual ~MethodSpy() = default; 21 | 22 | using CallArgs = std::tuple; 23 | 24 | virtual void assert_called() = 0; 25 | virtual void assert_called_once() = 0; 26 | virtual void assert_called_once_with(Args... args) = 0; 27 | virtual void assert_called_with(Args... args) = 0; 28 | virtual std::vector...>> call_args_list() = 0; 29 | }; 30 | 31 | class TestCase 32 | { 33 | public: 34 | TestCase(): 35 | self{*this} 36 | {} 37 | 38 | template 39 | void assertTrue(const T& value) 40 | { 41 | assertImpl(value, static_cast(value), "{value} is not True"); 42 | } 43 | 44 | template 45 | void assertFalse(const T& value) 46 | { 47 | assertImpl(value, !static_cast(value), "{value} is not False"); 48 | } 49 | 50 | template 51 | void assertIsNull(const T& value) 52 | { 53 | assertImpl(value, static_cast(value == nullptr), "{value} is not null"); 54 | } 55 | 56 | template 57 | void assertIsNotNull(const T& value) 58 | { 59 | assertImpl(value, static_cast(value != nullptr), "{value} is null"); 60 | } 61 | 62 | template 63 | void assertEqual(const T& value, const U& expected) 64 | { 65 | assertImpl(value, static_cast(value == expected), 66 | "{value} != {expected}", &expected); 67 | } 68 | 69 | template 70 | void assertNotEqual(const T& value, const U& expected) 71 | { 72 | assertImpl(value, static_cast(value == expected), 73 | "{value} == {expected}", &expected); 74 | } 75 | 76 | TestCase& self; 77 | 78 | private: 79 | template 80 | void assertImpl(const T& value, const bool result, const std::string& message, const U* expected = nullptr) 81 | { 82 | if(!result) 83 | { 84 | backward::StackTrace st; st.load_here(5); 85 | st.skip_n_firsts(4); 86 | 87 | if(expected) 88 | { 89 | throw unittest::detail::TestAssertionException{fmt::format( 90 | "Assertion error: " + message, fmt::arg("value", value), fmt::arg("expected", *expected)), 91 | std::move(st)}; 92 | } 93 | else 94 | { 95 | throw unittest::detail::TestAssertionException{fmt::format( 96 | "Assertion error: " + message, fmt::arg("value", value)), 97 | std::move(st)}; 98 | } 99 | } 100 | } 101 | }; 102 | 103 | } // namespace unittest 104 | 105 | #endif // UNITTEST_UNITTEST_HPP 106 | -------------------------------------------------------------------------------- /main.cpp.in: -------------------------------------------------------------------------------- 1 | #include 2 | ${include_tests} 3 | #include 4 | -------------------------------------------------------------------------------- /unittest.cmake: -------------------------------------------------------------------------------- 1 | enable_testing() 2 | 3 | function(add_unittest) 4 | cmake_parse_arguments( 5 | "ARGS" 6 | "" 7 | "NAME" 8 | "TESTS;DEPENDENCIES" 9 | ${ARGN} 10 | ) 11 | 12 | if(NOT ARGS_TESTS) 13 | message(FATAL_ERROR "Missing TESTS argument with set of tests headers") 14 | endif() 15 | 16 | if(NOT ARGS_NAME) 17 | message(FATAL_ERROR "Missing NAME argument with name of the test executable") 18 | endif() 19 | 20 | set(include_tests) 21 | 22 | foreach(header ${ARGS_TESTS}) 23 | get_filename_component(header_full_path "${header}" ABSOLUTE) 24 | set(include_tests "${include_tests}#include \"${header_full_path}\"\n") 25 | set(include_tests "${include_tests}#include \"${header_full_path}.tinyrefl\"\n") 26 | endforeach() 27 | 28 | set(main_file "${CMAKE_CURRENT_BINARY_DIR}/main.cpp") 29 | 30 | configure_file("${unittest_SOURCE_DIR}/main.cpp.in" "${main_file}") 31 | 32 | add_executable(${ARGS_NAME} ${ARGS_TESTS} ${main_file}) 33 | 34 | if(NOT TARGET unittest) 35 | message(FATAL_ERROR "unittest library target not found") 36 | endif() 37 | 38 | target_link_libraries(${ARGS_NAME} PRIVATE unittest) 39 | 40 | if(ARGS_DEPENDENCIES) 41 | target_link_libraries(${ARGS_NAME} PRIVATE ${ARGS_DEPENDENCIES}) 42 | endif() 43 | 44 | find_package(tinyrefl_tool REQUIRED) 45 | tinyrefl_tool(TARGET ${ARGS_NAME} HEADERS ${ARGS_TESTS}) 46 | 47 | add_test(NAME ${ARGS_NAME} COMMAND ${ARGS_NAME}) 48 | endfunction() 49 | --------------------------------------------------------------------------------