diff --git a/include/test.h b/include/test.h new file mode 100644 index 0000000..dfcee32 --- /dev/null +++ b/include/test.h @@ -0,0 +1,273 @@ +#ifndef TEST_H__ +#define TEST_H__ +#include +#include +#include +#include + +// Test lifecycle +// suite_setup_function(); - This is called to allocate any suite level resources. This is called once when the suite begins. +// These functions may be called in parallel but execution will not proceed past this block until they have all finished. +// test_setup_function(); - This is called once for every test in tests. You may use it to allocate resources or setup mocks, stubs, and spies. +// function_to_test(...); - This is called once for every test to execute the test. +// Only one of these test functions will actually be run for each test in tests. They should return true if the test passed, return false if the test failed or there was an error, and be nullptr if they should be skipped. The executed function will be called with expected_output and the result of function_to_test(...). They can be used to test functions with side effects, especially void functions. +// test_compare_function; - This is the highest priority compare function. If it is not nullptr then it will be called. +// suite_compare_function; - This is the second highest priority compare function. If test_compare_function is nullptr and this is not nullptr then it will be called. +// [](TResult expected, TResult actual) { return expected, actual; } - This is the lowest priority compare function. If all other compare functions are nullptr then this will be called to evaluate the test. +// test_teardown_function(); - This is called once for every test in tests. You must free/release any resources allocated by test_setup_function. +// This ends the parallel test functions section all tests will have completed before execution proceeds. +// Collect reports - Ths step is not visible to the user at this point, but data returned by all of the test functions is collected here. This is where you will eventually be able to format/log data for reports. +// suite_teardown_function(); - This is called after all test calls have completed, all test_teardown_function calls have completed, and all test reports/logs have been written. You should free any resources allocated in suite_setup_function. + +namespace Test { + using std::tuple; + using std::pair; + using std::vector; + using std::string; + + /// @brief + class TestResults { + public: + /// @brief Creates an empty TestResults instance representing no tests run. + TestResults(); + + /// @brief Creates a new TestResults instance that is a copy of other. + /// @param other + TestResults(const TestResults& other); + + /// @brief Creates a new TestResults instance with specific counts. + /// @param failed The number of failed tests. + /// @param passed The number of passed tests. + /// @param skipped The number of skipped tests. + /// @param total The total number of tests run. This should equal the sum of failed, passed, and skipped tests. + TestResults(uint32_t failed, uint32_t passed, uint32_t skipped, uint32_t total); + + /// @brief Adds a failed test. This increments total and failed. + /// @return A reference to this instance. Used for chaining. + TestResults& fail(); + + /// @brief Adds a passed test. This increments total and passed. + /// @return A reference to this instance. Used for chaining. + TestResults& pass(); + + /// @brief Adds a skipped test. This increments total and skipped. + /// @return A reference to this instance. Used for chaining. + TestResults& skip(); + + /// @brief Getter for the count of failed tests. + /// @return The count of failed tests. + uint32_t failed(); + + /// @brief Getter for the count of passed tests. + /// @return The count of passed tests. + uint32_t passed(); + + /// @brief Getter for the count of skipped tests. + /// @return The count of skipped tests. + uint32_t skipped(); + + /// @brief Getter for the count of total tests. + /// @return The count of total tests run. + uint32_t total(); + + /// @brief Returns the combination of this and another TestResults instance. + /// @param other The other TestResults instance to add to this one. + /// @return The combination of the two TestResults instances. + TestResults operator+(const TestResults& other) const; + + /// @brief Adds another TestResults to this one and returns a reference to this instance. + /// @param other The other TestResults instance to add to this one. + /// @return A reference to this instance. + TestResults& operator+=(const TestResults& other); + + private: + uint32_t failed_; + uint32_t passed_; + uint32_t skipped_; + uint32_t total_; + }; + + /// @brief + /// @tparam TResult + template + using TestCompareFunction = std::function; + + /// @brief + /// @tparam TResult + template + using MaybeTestCompareFunction = std::optional>; + + using TestConfigureFunction = std::function; + using MaybeTestConfigureFunction = std::optional; + + /// @brief + /// @tparam TResult + /// @tparam ...TInputParams + template + using TestTuple = std::tuple< + const std::string& /* test_name */, + const TResult& /* expected_output */, + std::tuple /* input_params - The input parameters for this test. These will be used when calling std::apply with function_to_test to execute the test. */, + MaybeTestCompareFunction /* test_compare_function - If this is not nullprt then this function will be called instead of suite_compare_function to determine if the test passes. Use this to check for side effects of the test. Return true if the test passes and false otherwise. */, + MaybeTestConfigureFunction /* test_setup_function - If this is not nullptr this function is called before each test to setup the environment. It is called with std::apply and input_params so you can use them to mock records with specific IDs or calculate an expected result. */, + MaybeTestConfigureFunction /* test_teardown_function If this is not nullptr this function is called after each test to cleanup any allocated/shared resources. */, + bool /* is_enabled If this is false the test, setup, and teardown functions are not run. */>; + + /// @brief + /// @tparam TResult + /// @tparam TFunctionToTest + /// @tparam ...TInputParams + template + using TestSuite = std::tuple< + std::string, + std::function, + std::vector>, + MaybeTestCompareFunction, + MaybeTestConfigureFunction, + MaybeTestConfigureFunction, + bool + >; + + // This function is called to execute a test suite. You provide it with some configuration info, optional utility callback functions, and test data (input parameters for each call to function_to_test and the expected result). It returns a TestResults that should be treated as an opaque data type. + // Not all parameters are named in code, but they are named and explained in the comments and will be described by those names below. + // string suite_name - This is the name of this test suite. It is used for reporting messages. + // TFunctionToTest function_to_test - This is the function to test. This may be replaced if necessary by std::function. It may not currently support class methods, but that is planned. + // vector> tests - This is the test run data. Each tuple in the vector is a single test run. It's members are explained below. + // string test_name - This is the name of this test. It is used for reporting messages. + // TResult expected_output - This is the expected result of executing this test. + // bool(*)(const TResult expected, const TResult actual) test_compare_function - This is optional. If unset or set to nullptr it is skipped. If set to a function it is called to evaluate the test results. It takes the expected and actual results as parameters and should return true if the test passed and false otherwise. This may be changed to return a TestResults at some point. + // void(*)(TInputParams...) test_setup_function - This is optional. If unset or set to nullptr it is skipped. If set to a function it is called before each test to setup the environment for the test. You may use it to allocate resources and setup mocks, stubs, and spies. + // void(*)(TInputParams...) test_teardown_function - This is optiona. If unset or set to nullptr it is skipped. If set to a function it is called after each test to cleanup the environment after the test. You should free resources allocated by test_setup_function. + // bool is_enabled - This is optional. If unset or set to true the test is run. If set to false this test is skipped. If skipped it will be reported as a skipped/disabled test. + // bool(*)(const TResult expected, const TResult actual) suite_compare_function - This is optional. If unset or set to nullptr it is skipped. If set to a function and test_compare_function is not called for a test run then this function is called to evaluate the test results. It takes the expected and actual results as parameters and should return true if the test passed and false otherwise. This may be changed to return a TestResults at some point. + // void(*)() suite_setup_function - This is optional. If unset or set to nullptr it is skipped. If set to a function it is called before starting this test suite to setup the environment. You may use it to allocate resources and setup mocks, stubs, and spies. + // void(*)() suite_teardown_function - This is optional. If unset or set to nullptr it is skipped. If set to a function it is called after all tests in this suite have finished and all reporting has finished. You should free resources allocated by suite_setup_function. + // This method should be called like so. This is the minimal call and omits all of the optional params. This is the most common usage. You should put one tuple of inputs and expected output for each test case. + // results = collect_and_report_test_resultstest_fn( + // "Test: function_under_test", + // function_under_test, + // vector({ + // make_tuple( + // "ShouldReturnAppleForGroupId_1_and_ItemId_2", + // string("Apple"), + // make_tuple(1,2), + // ), + // }), + // ); + // The suites can be run from one file as such. From a file called ThingDoer_test.cpp to test the class/methods ThingDoer declared in ThingDoer.cpp. This isn't mandatory but is a best practice. + // You can use function_to_test without calling collect_and_report_test_results() and also could call it from a normal int main(int argc, char** argv) or other function. + // TestResults test_main_ThingDoer(int argc, char** argv) { + // TestResults results; + // results = collect_and_report_test_results(results, function_to_test("do_thing1", ...), argc, argv); + // results = collect_and_report_test_results(results, function_to_test("do_thing2", ...), argc, argv); + // return results; + // } + // Then some test harness either generated or explicit can call test_main_ThingDoer(...) and optionally reported there. Reporting granularity is controlled by how frequently you call collect_and_report_test_results(...). + // You can combine test results with results = results + function_to_test(..); and then collect_and_report_test_results on the aggregate TestResults value. + + /// @brief + /// @tparam TResult The result type of the test. + /// @tparam TInputParams... The types of parameters sent to the test function. + /// @param suite_label The label for this test suite. For example a class name such as "MortgageCalculator". + /// @param function_to_test The function to be tested. It will be called with std::apply and a std::tuple made from each item in tests. + /// @param tests A std::vector of test runs. + /// @param suite_compare_function A function used to compare the expected and actual test results. This can be overridden per test by setting test_compare_function. + /// @param after_all This is called before each suite is started to setup the environment. This is where you should build mocks, setup spies, and test fixtures. + /// @param before_all This is called after each suite has completed to cleanup anything allocated in suite_before_each. + /// @param is_enabled If false none of these tests are run and they are all reported as skipped. + template + TestResults execute_suite( + std::string suite_label, + std::function function_to_test, + vector> tests, + MaybeTestCompareFunction suite_compare = std::nullopt, + MaybeTestConfigureFunction before_all = std::nullopt, + MaybeTestConfigureFunction after_all = std::nullopt, + bool is_enabled = true + ); + + /// @brief + /// @tparam TResult The result type of the test. + /// @tparam ...TInputParams The types of parameters sent to the test function. + /// @param test_name The label for this test. For example "should calculate the interest". + /// @param expected The expected output of calling the test function with these input parameters. + /// @param input_params The input parameters to use when calling the test function. + /// @param test_compare_fn An optional function that can be used to compare the expected and actual return values. This is good for when you only care about certain fields being equal. + /// @param before_each This is called to setup the environment before running the test. This is where you should build mocks, setup spies, and set any other values you need before calling the test function. + /// @param after_each This is called after each test run to cleanup anything allocated in before_each. + /// @param is_enabled If false this test run is not executed and considered skipped for reporting purposes. + /// @return A TestTuple suitable for use as a test run when calling test_fn. + template + TestTuple make_test( + const string& test_name, + const TResult& expected, + tuple input_params, + MaybeTestCompareFunction test_compare_fn = std::nullopt, + MaybeTestConfigureFunction before_each = std::nullopt, + MaybeTestConfigureFunction after_each = std::nullopt, + bool is_enabled = true) { + return make_tuple(test_name, expected, input_params, test_compare_fn, before_each, after_each, is_enabled); + } + + /// @brief + /// @tparam TResult + /// @tparam TFunctionToTest + /// @tparam ...TInputParams + /// @param suite_name + /// @param function_to_test + /// @param test_data + /// @param compare + /// @param before_each + /// @param after_each + /// @param is_enabled + /// @return + template + TestSuite make_test_suite( + const string& suite_name, + TFunctionToTest function_to_test, + vector> test_data, + MaybeTestCompareFunction compare = std::nullopt, + MaybeTestConfigureFunction before_each = std::nullopt, + MaybeTestConfigureFunction after_each = std::nullopt, + bool is_enabled = true) { + return make_tuple(suite_name, function_to_test, test_data, compare, before_each, after_each, is_enabled); + } + + template + TestResults execute_suite(const TestSuite& test_suite) { + return std::apply(execute_suite, test_suite); + } + + /// @brief + /// @tparam ...TInputParams + /// @param first + /// @param second + /// @return + template + MaybeTestConfigureFunction coalesce( + MaybeTestConfigureFunction first, + MaybeTestConfigureFunction second) { + if (first.has_value()) { + if (second.has_value()) { + // This is the only place we actually need to combine them. + return [&first, &second](TInputParams... input_params) { + *first(input_params...); + *second(input_params...); + }; + } else { + return first; + } + } else { + return second; + } + } +} // End namespace Test + +// TODO: define operator<< for std::tuple. +// TODO: Add TShared(*)(string /*test_name*/, UUID /*test_run_id*/) allocate_shared_data to the test tuple to make some shared data that can be used in a thread safe way by setup, teardown, and evaluate steps of the test. +// TODO: Add TShared to be returned by the setup functions, and consumed by the evaluate and teardown functions. +// Suite setup/teardown functions should allocate/free. +// Test setup/teardown functions should consume the data allocated by suite setup. +// Test setup functions may allocate additional resources. If they do then the allocated resources they should be freed by test teardown function. +// Suite and/or test compare functions may consume this shared data, but it will not be shared with the execution of function_to_test. +#endif // End !defined TEST_H__ diff --git a/src/test.cpp b/src/test.cpp new file mode 100644 index 0000000..1bb4574 --- /dev/null +++ b/src/test.cpp @@ -0,0 +1,295 @@ +#define _XOPEN_SOURCE_EXTENDED +#include +#include +#include +#include +#include +#include "test.h" + +// using namespace std; +using std::cout; +using std::tuple; +using std::wcerr; +using std::string; +using std::endl; +using std::pair; +using std::get; +using std::make_tuple; +using std::vector; +using Test::TestResults; +using std::optional; +using std::function; +using std::for_each; + +// using namespace Test; + +namespace Test { + const string fn1(const string& s, int l) { + return s.substr(0, l); + } + + // Test lifecycle + // suiteSetupFn(); - This is called to allocate any suite level resources. This is called once when the suite begins. + // These functions may be called in parallel but execution will not proceed past this block until they have all finished. + // testSetupFn(); - This is called once for every test in tests. You may use it to allocate resources or setup mocks, stubs, and spies. + // testFn(...); - This is called once for every test to execute the test. + // Only one of these test functions will actually be run for each test in tests. They should return true if the test passed, return false if the test failed or there was an error, and be nullptr if they should be skipped. The executed function will be called with expectedOutput and the result of testFn(...). They can be used to test functions with side effects, especially void functions. + // maybe_compare_function; - This is the highest priority compare function. If it is not nullptr then it will be called. + // suite_compare_function; - This is the second highest priority compare function. If maybe_compare_function is nullptr and this is not nullptr then it will be called. + // [](TResult expected, TResult actual) { return expected, actual; } - This is the lowest priority compare function. If all other compare functions are nullptr then this will be called to evaluate the test. + // testTeardownFn(); - This is called once for every test in tests. You must free/release any resources allocated by testSetupFn. + // This ends the parallel test functions section all tests will have completed before execution proceeds. + // Collect reports - Ths step is not visible to the user at this point, but data returned by all of the test functions is collected here. This is where you will eventually be able to format/log data for reports. + // suiteTeardownFn(); - This is called after all test calls have completed, all testTeardownFn calls have completed, and all test reports/logs have been written. You should free any resources allocated in suiteSetupFn. + + // TODO: Add TShared(*)(string /*test_name*/, UUID /*testRunId*/) allocateSharedData to the test tuple to make some shared data that can be used in a thread safe way by setup, teardown, and evaluate steps of the test. + // TODO: Add TShared to be returned by the setup functions, and consumed by the evaluate and teardown functions. + // Suite setup/teardown functions should allocate/free. + // Test setup/teardown functions should consume the data allocated by Suite setup. + // Test setup functions may allocate additional resources. If they do then the allocated resources they should be freed by test teardown function. + // Suite and/or Test compare functions may consume this shared data, but it will not be shared with the execution of testFn. + + // This function is called to execute a test suite. You provide it with some configuration info, optional utility callback functions, and test data (input parameters for each call to testFn and the expected result). It returns a TestResults that should be treated as an opaque data type. + // Not all parameters are named in code, but they are named and explained in the comments and will be described by those names below. + // string suite_label - This is the name of this test suite. It is used for reporting messages. + // FnToTest testFn - This is the function to test. This may be replaced if necessary by function. It may not currently support class methods, but that is planned. + // vector> tests - This is the test run data. Each tuple in the vector is a single test run. It's members are explained below. + // string test_name - This is the name of this test. It is used for reporting messages. + // TResult expectedOutput - This is the expected result of executing this test. + // bool(*)(const TResult expected, const TResult actual) maybe_compare_function - This is optional. If unset or set to nullptr it is skipped. If set to a function it is called to evaluate the test results. It takes the expected and actual results as parameters and should return true if the test passed and false otherwise. This may be changed to return a TestResults at some point. + // void(*)(TInputParams...) testSetupFn - This is optional. If unset or set to nullptr it is skipped. If set to a function it is called before each test to setup the environment for the test. You may use it to allocate resources and setup mocks, stubs, and spies. + // void(*)(TInputParams...) testTeardownFn - This is optiona. If unset or set to nullptr it is skipped. If set to a function it is called after each test to cleanup the environment after the test. You should free resources allocated by testSetupFn. + // bool isEnabled - This is optional. If unset or set to true the test is run. If set to false this test is skipped. If skipped it will be reported as a skipped/disabled test. + // bool(*)(const TResult expected, const TResult actual) suite_compare_function - This is optional. If unset or set to nullptr it is skipped. If set to a function and maybe_compare_function is not called for a test run then this function is called to evaluate the test results. It takes the expected and actual results as parameters and should return true if the test passed and false otherwise. This may be changed to return a TestResults at some point. + // void(*)() suiteSetupFn - This is optional. If unset or set to nullptr it is skipped. If set to a function it is called before starting this test suite to setup the environment. You may use it to allocate resources and setup mocks, stubs, and spies. + // void(*)() suiteTeardownFn - This is optional. If unset or set to nullptr it is skipped. If set to a function it is called after all tests in this suite have finished and all reporting has finished. You should free resources allocated by suiteSetupFn. + // This method should be called like so. This is the minimal call and omits all of the optional params. This is the most common usage. You should put one tuple of inputs and expected output for each test case. + // results = collect_and_report_TestResultstest_fn( + // "Test: functionUnderTest", + // functionUnderTest, + // vector({ + // make_tuple( + // "ShouldReturnAppleForGroupId_1_and_ItemId_2", + // string("Apple"), + // make_tuple(1,2), + // ), + // }), + // ); + // The suites can be run from one file as such. From a file called ThingDoer_test.cpp to test the class/methods ThingDoer declared in ThingDoer.cpp. This isn't mandatory but is a best practice. + // You can use testFn without calling collect_and_report_TestResults() and also could call it from a normal int main(int argc, char** argv) or other function. + // TestResults test_main_ThingDoer(int argc, char** argv) { + // TestResults results; + // results = collect_and_report_TestResults(results, testFn("doThing1", ...), argc, argv); + // results = collect_and_report_TestResults(results, testFn("doThing2", ...), argc, argv); + // return results; + // } + // Then some test harness either generated or explicit can call test_main_ThingDoer(...) and optionally reported there. Reporting granularity is controlled by how frequently you call collect_and_report_TestResults(...). + // You can combine test results with results = results + testFn(..); and then collect_and_report_TestResults on the aggregate TestResults value. + + template + TestResults execute_suite( + string suite_label, + function function_to_test, + vector> tests, + MaybeTestCompareFunction maybe_suite_compare_function, + MaybeTestConfigureFunction maybe_suite_before_each_function, + MaybeTestConfigureFunction maybe_suite_after_each_function, + bool is_enabled) { + TestResults results; + cout << "🚀 Beginning Suite: " << suite_label << endl; + + // Step 1: Suite Setup + + if (maybe_suite_before_each_function.has_value()) { + (*maybe_suite_before_each_function)(); + } + + // Step 2: Execute Tests + for_each(tests.begin(), tests.end(), [&suite_label, &function_to_test, &results, &maybe_suite_compare_function]( + TestTuple test_data + ) { + // Step 2a: Extract our variables from the TestTuple. + const std::string& test_name = get<0>(test_data); + const std::string qualified_test_name = suite_label + "::" + test_name; + const TResult& expected_output = get<1>(test_data); + std::tuple input_params = get<2>(test_data); + MaybeTestCompareFunction maybe_compare_function = get<3>(test_data); + TestCompareFunction compare_function = maybe_compare_function.has_value() + ? *maybe_compare_function + : maybe_suite_compare_function.has_value() + ? *maybe_suite_compare_function + : [](const TResult& l, const TResult& r){return l==r;}; + MaybeTestConfigureFunction before_each = get<4>(test_data); + MaybeTestConfigureFunction after_each = get<5>(test_data); + + // Step 2b: Test Setup + cout << " Beginning Test: " << qualified_test_name << endl; + if(before_each.has_value()) { + (*before_each)(); + } + + TResult actual; + try { + // Step 2c: Execute the test method. + actual = std::apply(function_to_test, input_params); + } catch(const std::exception& ex) { + cout << " ERROR: Caught exception \"" << ex.what() << "\"" << endl; + } catch(const std::string& message) { + cout << " ERROR: Caught string \"" << message << "\"" << endl; + } catch(...) { + cout << " ERROR: Caught something that is neither an std::exception nor a std::string." << endl; + } + + // Step 2d: Pass or fail. + TestResults result; + if (compare_function(expected_output, actual)) { + result = TestResults().pass(); + cout << " PASSED" << endl; + } else { + result = TestResults().fail(); + cout << " FAILED: expected: " << expected_output << ", actual: " << actual << endl; + } + results += result; + + // Step 2e: Test Teardown + if (after_each.has_value()) { + (*after_each)(); + } + cout << " Ending Test: " << test_name << endl; + }); + + // Step 3: Suite Teardown + if (maybe_suite_after_each_function.has_value()) { + (*maybe_suite_after_each_function)(); + } + cout << "Ending Suite: " << suite_label << endl; + return results; + } + + TestResults do_the_other_thing(){ + auto p1 = "Microsoft QBasic"; + auto p2 = 5; + // auto exp = "Micro"; + string s = fn1("Microsoft QBasic", 5); + TestResults tr; + + // tr = tr + execute_suite( + // "Test 8 Function", + // (function)fn1, + // vector>({ + // // vector, MaybeTestCompareFunction>>({ + // make_tuple( + // string("should do something"), // test_name + // string("Micro"), // expectedOutput + // make_tuple((string)p1, p2),// inputParams, + // std::nullopt, // compare_function + // std::nullopt, // before_each + // std::nullopt, // after_each + // true + // ), + // make_test( + // "should do something else", + // "Micro", + // make_tuple((string)p1, p2) + // ) + // })); + + auto test_data8 = vector>({ + make_test( + "Test 8 equals", "Micro", make_tuple((string)p1, p2), + [](const string& l, const string& r){ return l==r;}), + make_test( + "Test 8 not equals", "Micro", make_tuple((string)p1, p2), + [](const string& l, const string& r){ return l!=r;} + ), + make_test("Test 8 default compare", "Micro", make_tuple((string)p1, p2)), + make_test("Test 8 default compare", "Micro", make_tuple((string)p1, p2)), + make_test("Test 8 default compare", "Micro", make_tuple((string)p1, p2)) + }); + tr = tr + execute_suite( + "Test 8 Function with extra data", + (function)fn1, + test_data8 + ); + + return tr; + } + + // _Step_9 - if T2 is a single value then make_tuple(T2) and call longer version + // auto testFunction = [](int id){return id==0?"":"";}; + // auto compareFunction = [](const string a, const string b){return a==b;}; + // template + + // _Step_10 - + // test_fn(string, _FnToTest, vector>>) + // Default to (string, _FnToTest, vector