This commit is contained in:
2023-05-02 20:34:48 -07:00
parent ab4ac26aed
commit 072345f474
4 changed files with 1695 additions and 267 deletions

2
BUILD
View File

@@ -4,7 +4,7 @@ cc_library(
name = "tinytest", name = "tinytest",
srcs = ["tinytest.cpp"], srcs = ["tinytest.cpp"],
hdrs = ["tinytest.h"], hdrs = ["tinytest.h"],
includes = ["."], includes = ["*.h"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )

View File

@@ -64,78 +64,78 @@ TestResults::TestResults(uint32_t errors,
skipped_(skipped), skipped_(skipped),
total_(total) {} total_(total) {}
TestResults& TestResults::error() { TestResults& TestResults::Error() {
errors_++; errors_++;
return *this; return *this;
} }
TestResults& TestResults::error(string message) { TestResults& TestResults::Error(string message) {
errors_++; errors_++;
error_messages_.push_back(message); error_messages_.push_back(message);
return *this; return *this;
} }
TestResults& TestResults::fail() { TestResults& TestResults::Fail() {
total_++; total_++;
failed_++; failed_++;
return *this; return *this;
} }
TestResults& TestResults::fail(const string& message) { TestResults& TestResults::Fail(const string& message) {
total_++; total_++;
failed_++; failed_++;
failure_messages_.push_back(message); failure_messages_.push_back(message);
return *this; return *this;
} }
vector<string> TestResults::failure_messages() { vector<string> TestResults::FailureMessages() const {
return failure_messages_; return failure_messages_;
} }
TestResults& TestResults::pass() { TestResults& TestResults::Pass() {
total_++; total_++;
passed_++; passed_++;
return *this; return *this;
} }
TestResults& TestResults::skip() { TestResults& TestResults::Skip() {
total_++; total_++;
skipped_++; skipped_++;
return *this; return *this;
} }
TestResults& TestResults::skip(const string& message) { TestResults& TestResults::Skip(const string& message) {
total_++; total_++;
skipped_++; skipped_++;
skip_messages_.push_back(message); skip_messages_.push_back(message);
return *this; return *this;
} }
vector<string> TestResults::skip_messages() { vector<string> TestResults::SkipMessages() const {
return skip_messages_; return skip_messages_;
} }
vector<string> TestResults::error_messages() { vector<string> TestResults::ErrorMessages() const {
return error_messages_; return error_messages_;
} }
uint32_t TestResults::errors() { uint32_t TestResults::Errors() const {
return errors_; return errors_;
} }
uint32_t TestResults::failed() { uint32_t TestResults::Failed() const {
return failed_; return failed_;
} }
uint32_t TestResults::passed() { uint32_t TestResults::Passed() const {
return passed_; return passed_;
} }
uint32_t TestResults::skipped() { uint32_t TestResults::Skipped() const {
return skipped_; return skipped_;
} }
uint32_t TestResults::total() { uint32_t TestResults::Total() const {
return total_; return total_;
} }
@@ -173,32 +173,32 @@ TestResults& TestResults::operator+=(const TestResults& other) {
} }
void PrintResults(std::ostream& os, TestResults results) { void PrintResults(std::ostream& os, TestResults results) {
auto skip_messages = results.skip_messages(); auto skip_messages = results.SkipMessages();
if (skip_messages.size() > 0) { if (skip_messages.size() > 0) {
os << "Skipped:" << endl; os << "Skipped:" << endl;
for_each(skip_messages.begin(), skip_messages.end(), [&os](const string& message) { for_each(skip_messages.begin(), skip_messages.end(), [&os](const string& message) {
os << "🚧Skipped: " << message << endl; os << "🚧Skipped: " << message << endl;
}); });
} }
auto failure_messages = results.failure_messages(); auto failure_messages = results.FailureMessages();
if (failure_messages.size() > 0) { if (failure_messages.size() > 0) {
os << "Failures:" << endl; os << "Failures:" << endl;
for_each(failure_messages.begin(), failure_messages.end(), [&os](const string& message) { for_each(failure_messages.begin(), failure_messages.end(), [&os](const string& message) {
os << "❌FAILED: " << message << endl; os << "❌FAILED: " << message << endl;
}); });
} }
auto error_messages = results.error_messages(); auto error_messages = results.ErrorMessages();
if (error_messages.size() > 0) { if (error_messages.size() > 0) {
os << "Errors:" << endl; os << "Errors:" << endl;
for_each(error_messages.begin(), error_messages.end(), [&os](const string& message) { for_each(error_messages.begin(), error_messages.end(), [&os](const string& message) {
os << "🔥ERROR: " << message << endl; os << "🔥ERROR: " << message << endl;
}); });
} }
os << "Total tests: " << results.total() << endl; os << "Total tests: " << results.Total() << endl;
os << "Passed: " << results.passed() << "" << endl; os << "Passed: " << results.Passed() << "" << endl;
os << "Failed: " << results.failed() << "" << endl; os << "Failed: " << results.Failed() << "" << endl;
os << "Skipped: " << results.skipped() << " 🚧" << endl; os << "Skipped: " << results.Skipped() << " 🚧" << endl;
os << "Errors: " << results.errors() << " 🔥" << endl; os << "Errors: " << results.Errors() << " 🔥" << endl;
} }
// End TestResults methods. // End TestResults methods.
@@ -206,4 +206,41 @@ void PrintResults(std::ostream& os, TestResults results) {
MaybeTestConfigureFunction DefaultTestConfigureFunction() { MaybeTestConfigureFunction DefaultTestConfigureFunction() {
return std::nullopt; return std::nullopt;
} }
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]() {
first.value()();
second.value()();
};
} else {
return first;
}
} else {
return second;
}
}
// Utility functions.
TestResults& SkipTest(TestResults& results,
const std::string& suite_label,
const std::string& test_label,
std::optional<const std::string> reason) {
std::string qualified_test_label = suite_label + "::" + test_label;
std::cout << " 🚧Skipping Test: " << test_label;
if (reason.has_value()) {
std::cout << " because " << reason.value();
}
std::cout << std::endl;
results.Skip(qualified_test_label + (reason.has_value() ? " because " + reason.value() : ""));
return results;
}
// TODO: Factor out the pretty printing into a separate module so it can be tested separately.
// TODO: Consider making separate files for test suite, tests, test cases, and test results.
// TODO: Come up with a way to autogenerat a main function that runs all tests in a *_test.cpp file.
// TODO: Come up with a way to aggregate TestResults over multiple c++ files when running under bazel.
// TODO: Create a Makefile to build as a library.
} // namespace TinyTest } // namespace TinyTest

View File

@@ -10,58 +10,154 @@
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <initializer_list>
#include <iostream> #include <iostream>
#include <optional> #include <optional>
#include <regex>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view>
#include <tuple> #include <tuple>
#include <utility> #include <utility>
#include <vector> #include <vector>
// TODO: Document this. namespace TinyTest {
// Tuple printer from: // Begin EscapeForPrinting
// https://stackoverflow.com/questions/6245735/pretty-print-stdtuple/31116392#58417285 template <typename TChar, typename TTraits>
template <typename TChar, typename TTraits, typename... TArgs> std::basic_string_view<TChar, TTraits> EscapeForPrinting(const std::basic_string_view<TChar, TTraits>& text) {
auto& operator<<(std::basic_ostream<TChar, TTraits>& os, std::tuple<TArgs...> const& t) { return std::regex_replace(text, std::regex("\033"), "\\033");
std::apply([&os](auto&&... args) { ((os << args << " "), ...); }, t); }
template <typename TChar, typename TTraits>
std::basic_string<TChar, TTraits> EscapeForPrinting(const std::basic_string<TChar, TTraits>& text) {
return std::regex_replace(text, std::regex("\033"), "\\033");
}
template <typename TChar>
std::basic_string<TChar> EscapeForPrinting(const TChar* text) {
return std::regex_replace(text, std::regex("\033"), "\\033");
}
// End EscapeForPrinting
// Begin PrettyPrint
// std::string_view.
template <typename TChar, typename TTraits>
auto& PrettyPrint(std::basic_ostream<TChar, TTraits>& os, const std::basic_string_view<TChar, TTraits>& item) {
os << "\"" << EscapeForPrinting(item) << "\"";
return os; return os;
} }
// TODO: Document this. // std::string.
template <typename TChar, typename TTraits, typename TItem> template <typename TChar, typename TTraits>
auto& operator<<(std::basic_ostream<TChar, TTraits>& os, std::vector<TItem> v) { auto& PrettyPrint(std::basic_ostream<TChar, TTraits>& os, const std::basic_string<TChar, TTraits>& item) {
os << "\"" << EscapeForPrinting(item) << "\"";
return os;
}
// const char*.
template <typename TChar, typename TTraits>
auto& PrettyPrint(std::basic_ostream<TChar, TTraits>& os, const TChar* item) {
os << "\"" << EscapeForPrinting(item) << "\"";
return os;
}
// tuple<...>
template <typename TChar, typename TTraits, typename... TArgs>
auto& PrettyPrint(std::basic_ostream<TChar, TTraits> os, const std::tuple<TArgs...>& tuple) {
std::apply(
[&os](auto&&... args) {
if (sizeof...(TArgs) == 0) {
os << "[]";
return;
}
size_t n = 0;
os << "[ "; os << "[ ";
for (auto it = v.begin(); it != v.end(); it++) { ((PrettyPrint(os, args) << (++n != sizeof...(TArgs) ? ", " : "")), ...);
if (it != v.begin()) { os << " ]";
},
tuple);
return os;
}
// containers
template <typename TChar,
typename TTraits,
typename TContainer,
typename = std::enable_if_t<
std::is_same_v<decltype(std::declval<TContainer>().begin()), decltype(std::declval<TContainer>().end())>>,
typename = std::enable_if_t<std::is_base_of_v<
std::input_iterator_tag,
typename std::iterator_traits<decltype(std::declval<TContainer>().begin())>::iterator_category>>>
auto& PrettyPrint(std::basic_ostream<TChar, TTraits>& os, TContainer container) {
os << "[ ";
for (auto it = container.begin(); it != container.end(); it++) {
if (it != container.begin()) {
os << ", "; os << ", ";
} }
os << *it; PrettyPrint(os, *it);
} }
os << " ]"; os << " ]";
return os; return os;
} }
// TODO: Document this. // Catch-all for everything else.
template <typename TChar, typename TTraits, typename TItem> template <typename TChar, typename TTraits, typename TItem>
auto& compare(std::basic_ostream<TChar, TTraits>& error_message, auto& PrettyPrint(std::basic_ostream<TChar, TTraits>& os, TItem item) {
std::vector<TItem> expected, os << item;
std::vector<TItem> actual) { return os;
if (expected.size() != actual.size()) {
error_message << "size mismatch expected: " << expected.size() << ", actual: " << actual.size();
return error_message;
} }
for (size_t index = 0; index < expected.size(); index++) { // End PrettyPrint
if (expected[index] != actual[index]) {
error_message << "vectors differ at index " << index << ", \"" << expected[index] << "\" != \"" << actual[index] // Begin PrettyPrintWithSeparator
<< "\", expected: \"" << expected << "\", actual: \"" << actual << "\""; // Prints args with separator between them. std::string_view separator.
return error_message; template <typename TChar, typename TTraits, typename... TArgs>
} auto& PrettyPrintWithSeparator(std::basic_ostream<TChar, TTraits> os,
} std::basic_string_view<TChar, TTraits> separator,
return error_message; TArgs&&... args) {
(((PrettyPrint(os, args), os), EscapeForPrinting(separator)), ...);
return os;
}
// Prints args with separator between them. std::string separator.
template <typename TChar, typename TTraits, typename... TArgs>
auto& PrettyPrintWithSeparator(std::basic_ostream<TChar, TTraits> os,
std::basic_string<TChar, TTraits> separator,
TArgs&&... args) {
(((PrettyPrint_item(os, args), os), EscapeForPrinting(separator)), ...);
return os;
}
// Prints args with separator between them. const char* separator.
template <typename TChar, typename TTraits, typename... Args>
auto& PrettyPrintWithSeparator(std::basic_ostream<TChar, TTraits> os, const TChar* separator, Args&&... args) {
((os << args << EscapeForPrinting(separator)), ...);
return os;
}
// End PrettyPrintWithSeparator
////////////////////
// TODO: Document this.
template <typename TResult, typename... TParameters>
std::string InterceptCout(std::function<TResult(TParameters...)> fnToExecute,
std::optional<std::tuple<TParameters...>> maybe_args = std::nullopt) {
std::ostringstream os;
auto saved_buffer = std::cout.rdbuf();
std::cout.rdbuf(os.rdbuf());
// TODO: run the function
if (maybe_args.has_value()) {
std::apply(fnToExecute, maybe_args.value());
} else {
std::invoke(fnToExecute);
}
std::cout.rdbuf(saved_buffer);
return os.str();
} }
namespace TinyTest {
/// @brief /// @brief
class TestResults { class TestResults {
public: public:
@@ -93,69 +189,69 @@ class TestResults {
/// @brief Adds an error. This increments errors. /// @brief Adds an error. This increments errors.
/// @return A reference to this instance. Used for chaining. /// @return A reference to this instance. Used for chaining.
TestResults& error(); TestResults& Error();
/// @brief Adds an error with a message. This increments errors as well as /// @brief Adds an error with a message. This increments errors as well as
/// saving the error message. /// saving the error message.
/// @param message The error message. /// @param message The error message.
/// @return A reference to this instance. Used for chaining. /// @return A reference to this instance. Used for chaining.
TestResults& error(std::string message); TestResults& Error(std::string message);
/// @brief Adds a failed test. This increments total and failed. /// @brief Adds a failed test. This increments total and failed.
/// @return A reference to this instance. Used for chaining. /// @return A reference to this instance. Used for chaining.
TestResults& fail(); TestResults& Fail();
/// @brief Adds a failed test with a message. This increments total and failed /// @brief Adds a failed test with a message. This increments total and failed
/// as well as saving the failure message. /// as well as saving the failure message.
/// @param message The reason the test failed. /// @param message The reason the test failed.
/// @return A reference to this instance. Used for chaining. /// @return A reference to this instance. Used for chaining.
TestResults& fail(const std::string& message); TestResults& Fail(const std::string& message);
/// @brief Adds a passed test. This increments total and passed. /// @brief Adds a passed test. This increments total and passed.
/// @return A reference to this instance. Used for chaining. /// @return A reference to this instance. Used for chaining.
TestResults& pass(); TestResults& Pass();
/// @brief Adds a skipped test. This increments total and skipped. /// @brief Adds a skipped test. This increments total and skipped.
/// @return A reference to this instance. Used for chaining. /// @return A reference to this instance. Used for chaining.
TestResults& skip(); TestResults& Skip();
/// @brief Adds a skipped test with a message. This increments total and /// @brief Adds a skipped test with a message. This increments total and
/// skipped as well as saving the skip message. /// skipped as well as saving the skip message.
/// @param message The reason the test was skipped. /// @param message The reason the test was skipped.
/// @return A reference to this instance. Used for chaining. /// @return A reference to this instance. Used for chaining.
TestResults& skip(const std::string& message); TestResults& Skip(const std::string& message);
/// @brief Getter for the list of error messages. /// @brief Getter for the list of error messages.
/// @return /// @return
std::vector<std::string> error_messages(); std::vector<std::string> ErrorMessages() const;
/// @brief Getter for the count of errors. /// @brief Getter for the count of errors.
/// @return /// @return
uint32_t errors(); uint32_t Errors() const;
/// @brief Getter for the count of failed tests. /// @brief Getter for the count of failed tests.
/// @return The count of failed tests. /// @return The count of failed tests.
uint32_t failed(); uint32_t Failed() const;
/// @brief Getter for the list of failure messages. /// @brief Getter for the list of failure messages.
/// @return The list of failure messages. /// @return The list of failure messages.
std::vector<std::string> failure_messages(); std::vector<std::string> FailureMessages() const;
/// @brief Getter for the count of passed tests. /// @brief Getter for the count of passed tests.
/// @return The count of passed tests. /// @return The count of passed tests.
uint32_t passed(); uint32_t Passed() const;
/// @brief Getter for the count of skipped tests. /// @brief Getter for the count of skipped tests.
/// @return The count of skipped tests. /// @return The count of skipped tests.
uint32_t skipped(); uint32_t Skipped() const;
/// @brief Getter for the list of skip messages. /// @brief Getter for the list of skip messages.
/// @return The list of skip messages. /// @return The list of skip messages.
std::vector<std::string> skip_messages(); std::vector<std::string> SkipMessages() const;
/// @brief Getter for the count of total tests. /// @brief Getter for the count of total tests.
/// @return The count of total tests run. /// @return The count of total tests run.
uint32_t total(); uint32_t Total() const;
/// @brief Returns the combination of this and another TestResults instance. /// @brief Returns the combination of this and another TestResults instance.
/// @param other The other TestResults instance to add to this one. /// @param other The other TestResults instance to add to this one.
@@ -214,8 +310,8 @@ using TestTuple =
std::tuple<TInputParams...> /* input_params - The input parameters for this test. These will be used when std::tuple<TInputParams...> /* input_params - The input parameters for this test. These will be used when
calling std::apply with function_to_test to execute the test. */ calling std::apply with function_to_test to execute the test. */
, ,
MaybeTestCompareFunction<TResult> /* test_compare_function - If this is not nullprt then this function MaybeTestCompareFunction<TResult> /* test_Compare_function - If this is not nullprt then this function
will be called instead of suite_compare_function to determine if the 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 test passes. Use this to check for side effects of the test. Return
true if the test passes and false otherwise. */ true if the test passes and false otherwise. */
, ,
@@ -236,89 +332,17 @@ using TestTuple =
template <typename TResult, typename... TInputParams> template <typename TResult, typename... TInputParams>
using TestSuite = std::tuple<std::string, using TestSuite = std::tuple<std::string,
std::function<TResult(TInputParams...)>, std::function<TResult(TInputParams...)>,
std::vector<TestTuple<TResult, TInputParams...>>, std::initializer_list<TestTuple<TResult, TInputParams...>>,
MaybeTestCompareFunction<TResult>, MaybeTestCompareFunction<TResult>,
MaybeTestConfigureFunction, MaybeTestConfigureFunction,
MaybeTestConfigureFunction, MaybeTestConfigureFunction,
bool>; bool>;
// This function is called to execute a test suite. You provide it with some TestResults& SkipTest(TestResults& results,
// configuration info, optional utility callback functions, and test data (input const std::string& suite_label,
// parameters for each call to function_to_test and the expected result). It const std::string& test_label,
// returns a TestResults that should be treated as an opaque data type. Not all std::optional<const std::string> reason = std::nullopt);
// 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<tuple<...>>
// 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 /// @brief
/// @tparam TResult The result type of the test. /// @tparam TResult The result type of the test.
/// @tparam TInputParams... The types of parameters sent to the test function. /// @tparam TInputParams... The types of parameters sent to the test function.
@@ -327,25 +351,36 @@ using TestSuite = std::tuple<std::string,
/// @param function_to_test The function to be tested. It will be called with /// @param function_to_test The function to be tested. It will be called with
/// std::apply and a std::tuple<TInputParams...> made from each item in tests. /// std::apply and a std::tuple<TInputParams...> made from each item in tests.
/// @param tests A std::vector of test runs. /// @param tests A std::vector of test runs.
/// @param suite_compare_function A function used to compare the expected and /// @param suite_Compare A function used to Compare the expected and actual test
/// actual test results. This can be overridden per test by setting /// results. This can be overridden per test by setting test_Compare.
/// test_compare_function.
/// @param after_all This is called before each suite is started to setup the /// @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 /// environment. This is where you should build mocks, setup spies, and test
/// fixtures. /// fixtures.
/// @param before_all This is called after each suite has completed to cleanup /// @param before_all This is called after each suite has completed to cleanup
/// anything allocated in suite_before_each. /// anything allocated in suite_before_each.
/// @param is_enabled If false none of these tests are run and they are all /// @param is_enabled If false the test is reported as skipped. If true the test
/// reported as skipped. /// is run as normal.
template <typename TResult, typename... TInputParams> template <typename TResult, typename... TInputParams>
TestResults execute_suite(std::string suite_label, TestResults ExecuteSuite(std::string suite_label,
std::function<TResult(TInputParams...)> function_to_test, std::function<TResult(TInputParams...)> function_to_test,
std::vector<TestTuple<TResult, TInputParams...>> tests, std::initializer_list<TestTuple<TResult, TInputParams...>> tests,
MaybeTestCompareFunction<TResult> suite_compare = std::nullopt, MaybeTestCompareFunction<TResult> suite_Compare = std::nullopt,
MaybeTestConfigureFunction before_all = std::nullopt, MaybeTestConfigureFunction before_all = std::nullopt,
MaybeTestConfigureFunction after_all = std::nullopt, MaybeTestConfigureFunction after_all = std::nullopt,
bool is_enabled = true) { bool is_enabled = true) {
TestResults results; TestResults results;
if (!is_enabled) {
std::cout << "🚧Skipping suite: " << suite_label << " because it is disabled." << std::endl;
for (auto test : tests) {
std::string test_label = std::get<0>(test);
SkipTest(results, suite_label, test_label, "the suite is disabled.");
}
return results;
}
if (tests.size() == 0) {
std::cout << "🚧Skipping suite: " << suite_label << " because it is empty." << std::endl;
return results;
}
std::cout << "🚀Beginning Suite: " << suite_label << std::endl; std::cout << "🚀Beginning Suite: " << suite_label << std::endl;
// Step 1: Suite Setup // Step 1: Suite Setup
@@ -357,29 +392,28 @@ TestResults execute_suite(std::string suite_label,
// Step 2: Execute Tests // Step 2: Execute Tests
for_each(tests.begin(), for_each(tests.begin(),
tests.end(), tests.end(),
[&suite_label, &function_to_test, &results, &suite_compare](TestTuple<TResult, TInputParams...> test_data) { [&suite_label, &function_to_test, &results, &suite_Compare](TestTuple<TResult, TInputParams...> test_data) {
// Step 2a: Extract our variables from the TestTuple. // Step 2a: Extract our variables from the TestTuple.
const std::string& test_name = std::get<0>(test_data); const std::string& test_label = std::get<0>(test_data);
const std::string qualified_test_name = suite_label + "::" + test_name; const std::string qualified_test_label = suite_label + "::" + test_label;
const TResult& expected_output = std::get<1>(test_data); const TResult& expected_output = std::get<1>(test_data);
std::tuple<TInputParams...> input_params = std::get<2>(test_data); std::tuple<TInputParams...> input_params = std::get<2>(test_data);
MaybeTestCompareFunction<TResult> maybe_compare_function = std::get<3>(test_data); MaybeTestCompareFunction<TResult> maybe_Compare_function = std::get<3>(test_data);
TestCompareFunction<TResult> compare_function = TestCompareFunction<TResult> Compare_function =
maybe_compare_function.has_value() ? *maybe_compare_function maybe_Compare_function.has_value() ? *maybe_Compare_function
: suite_compare.has_value() ? *suite_compare : suite_Compare.has_value() ? *suite_Compare
: [](const TResult& l, const TResult& r) { return l == r; }; : [](const TResult& l, const TResult& r) { return l == r; };
MaybeTestConfigureFunction before_each = std::get<4>(test_data); MaybeTestConfigureFunction before_each = std::get<4>(test_data);
MaybeTestConfigureFunction after_each = std::get<5>(test_data); MaybeTestConfigureFunction after_each = std::get<5>(test_data);
bool is_enabled = std::get<6>(test_data); bool is_enabled = std::get<6>(test_data);
if (!is_enabled) { if (!is_enabled) {
std::cout << " 🚧Skipping Test: " << test_name << std::endl; SkipTest(results, suite_label, test_label);
results.skip(qualified_test_name);
return; return;
} }
// Step 2b: Test Setup // Step 2b: Test Setup
std::cout << " Beginning Test: " << test_name << std::endl; std::cout << " Beginning Test: " << test_label << std::endl;
if (before_each.has_value()) { if (before_each.has_value()) {
(*before_each)(); (*before_each)();
} }
@@ -390,30 +424,35 @@ TestResults execute_suite(std::string suite_label,
actual = std::apply(function_to_test, input_params); actual = std::apply(function_to_test, input_params);
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
std::ostringstream os; std::ostringstream os;
os << "Caught exception \"" << ex.what() << "\""; os << "Caught exception \"" << ex.what() << "\".";
results.error(qualified_test_name + " " + os.str()); results.Error(qualified_test_label + " " + os.str());
std::cout << " 🔥ERROR: " << os.str() << std::endl; std::cout << " 🔥ERROR: " << os.str() << std::endl;
} catch (const std::string& message) { } catch (const std::string& message) {
std::ostringstream os; std::ostringstream os;
os << "Caught string \"" << message << "\""; os << "Caught string \"" << message << "\".";
results.error(qualified_test_name + " " + os.str()); results.Error(qualified_test_label + " " + os.str());
std::cout << " 🔥ERROR: " << os.str() << std::endl;
} catch (const char* message) {
std::ostringstream os;
os << "Caught c-string \"" << message << "\".";
results.Error(qualified_test_label + " " + os.str());
std::cout << " 🔥ERROR: " << os.str() << std::endl; std::cout << " 🔥ERROR: " << os.str() << std::endl;
} catch (...) { } catch (...) {
std::string message = std::string message =
"Caught something that is neither an std::exception " "Caught something that is neither an std::exception "
"nor an std::string."; "nor an std::string.";
results.error(qualified_test_name + " " + message); results.Error(qualified_test_label + " " + message);
std::cout << " 🔥ERROR: " << message << std::endl; std::cout << " 🔥ERROR: " << message << std::endl;
} }
// Step 2d: Pass or fail. // Step 2d: Pass or fail.
if (compare_function(expected_output, actual)) { if (Compare_function(expected_output, actual)) {
results.pass(); results.Pass();
std::cout << " ✅PASSED" << std::endl; std::cout << " ✅PASSED" << std::endl;
} else { } else {
std::ostringstream os; std::ostringstream os;
os << "expected: \"" << expected_output << "\", actual: \"" << actual << "\""; os << "expected: \"" << expected_output << "\", actual: \"" << actual << "\"";
results.fail(qualified_test_name + " " + os.str()); results.Fail(qualified_test_label + " " + os.str());
std::cout << " ❌FAILED: " << os.str() << std::endl; std::cout << " ❌FAILED: " << os.str() << std::endl;
} }
@@ -421,7 +460,7 @@ TestResults execute_suite(std::string suite_label,
if (after_each.has_value()) { if (after_each.has_value()) {
(*after_each)(); (*after_each)();
} }
std::cout << " Ending Test: " << test_name << std::endl; std::cout << " Ending Test: " << test_label << std::endl;
}); });
// Step 3: Suite Teardown // Step 3: Suite Teardown
@@ -435,30 +474,17 @@ TestResults execute_suite(std::string suite_label,
/// @brief /// @brief
/// @tparam TResult The result type of the test. /// @tparam TResult The result type of the test.
/// @tparam TInputParams... The types of parameters sent to the test function. /// @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 /// @param test_suite A tuple representing the test suite configuration.
/// such as "MortgageCalculator".
/// @param function_to_test The function to be tested. It will be called with
/// std::apply and a std::tuple<TInputParams...> made from each item in tests.
/// @param tests A std::vector of test runs.
/// @param suite_compare A function used to compare the expected and actual test
/// results. This can be overridden per test by setting test_compare.
/// @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 the test is reported as skipped. If true the test
/// is run as normal.
template <typename TResult, typename... TInputParams> template <typename TResult, typename... TInputParams>
TestResults execute_suite(std::string suite_label, TestResults ExecuteSuite(const TestSuite<TResult, TInputParams...>& test_suite) {
std::function<TResult(TInputParams...)> function_to_test, std::string suite_label = std::get<0>(test_suite);
std::initializer_list<TestTuple<TResult, TInputParams...>> tests, std::function<TResult(TInputParams...)> function_to_test = std::get<1>(test_suite);
MaybeTestCompareFunction<TResult> suite_compare = std::nullopt, std::initializer_list<TestTuple<TResult, TInputParams...>> tests = std::get<2>(test_suite);
MaybeTestConfigureFunction before_all = std::nullopt, MaybeTestCompareFunction<TResult> suite_Compare = sizeof(test_suite) > 3 ? std::get<3>(test_suite) : std::nullopt;
MaybeTestConfigureFunction after_all = std::nullopt, MaybeTestConfigureFunction before_all = sizeof(test_suite) > 4 ? std::get<4>(test_suite) : std::nullopt;
bool is_enabled = true) { MaybeTestConfigureFunction after_all = sizeof(test_suite) > 5 ? std::get<5>(test_suite) : std::nullopt;
std::vector test_data = std::vector(tests); bool is_enabled = sizeof(test_suite) > 6 ? std::get<6>(test_suite) : true;
return execute_suite(suite_label, function_to_test, tests, suite_compare, before_all, after_all, is_enabled); return ExecuteSuite(suite_label, function_to_test, tests, suite_Compare, before_all, after_all, is_enabled);
} }
/// @brief /// @brief
@@ -470,7 +496,7 @@ TestResults execute_suite(std::string suite_label,
/// input parameters. /// input parameters.
/// @param input_params The input parameters to use when calling the test /// @param input_params The input parameters to use when calling the test
/// function. /// function.
/// @param test_compare_fn An optional function that can be used to compare the /// @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 /// expected and actual return values. This is good for when you only care about
/// certain fields being equal. /// certain fields being equal.
/// @param before_each This is called to setup the environment before running /// @param before_each This is called to setup the environment before running
@@ -482,14 +508,14 @@ TestResults execute_suite(std::string suite_label,
/// skipped for reporting purposes. /// skipped for reporting purposes.
/// @return A TestTuple suitable for use as a test run when calling test_fn. /// @return A TestTuple suitable for use as a test run when calling test_fn.
template <typename TResult, typename... TInputParams> template <typename TResult, typename... TInputParams>
TestTuple<TResult, TInputParams...> make_test(const std::string& test_name, TestTuple<TResult, TInputParams...> MakeTest(const std::string& test_name,
const TResult& expected, const TResult& expected,
std::tuple<TInputParams...> input_params, std::tuple<TInputParams...> input_params,
MaybeTestCompareFunction<TResult> test_compare_fn = std::nullopt, MaybeTestCompareFunction<TResult> test_Compare_fn = std::nullopt,
MaybeTestConfigureFunction before_each = std::nullopt, MaybeTestConfigureFunction before_each = std::nullopt,
MaybeTestConfigureFunction after_each = std::nullopt, MaybeTestConfigureFunction after_each = std::nullopt,
bool is_enabled = true) { bool is_enabled = true) {
return make_tuple(test_name, expected, input_params, test_compare_fn, before_each, after_each, is_enabled); return make_tuple(test_name, expected, input_params, test_Compare_fn, before_each, after_each, is_enabled);
} }
/// @brief /// @brief
@@ -499,71 +525,114 @@ TestTuple<TResult, TInputParams...> make_test(const std::string& test_name,
/// @param suite_name /// @param suite_name
/// @param function_to_test /// @param function_to_test
/// @param test_data /// @param test_data
/// @param compare /// @param Compare
/// @param before_each /// @param before_each
/// @param after_each /// @param after_each
/// @param is_enabled /// @param is_enabled
/// @return /// @return
template <typename TResult, typename TFunctionToTest, typename... TInputParams> template <typename TResult, typename TFunctionToTest, typename... TInputParams>
TestSuite<TResult, TInputParams...> make_test_suite(const std::string& suite_name, TestSuite<TResult, TInputParams...> MakeTestSuite(const std::string& suite_name,
TFunctionToTest function_to_test,
std::vector<TestTuple<TResult, TInputParams...>> test_data,
MaybeTestCompareFunction<TResult> 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 <typename TResult, typename TFunctionToTest, typename... TInputParams>
TestSuite<TResult, TInputParams...> make_test_suite(
const std::string& suite_name,
TFunctionToTest function_to_test, TFunctionToTest function_to_test,
std::initializer_list<TestTuple<TResult, TInputParams...>> test_data, std::initializer_list<TestTuple<TResult, TInputParams...>> test_data,
MaybeTestCompareFunction<TResult> compare = std::nullopt, MaybeTestCompareFunction<TResult> Compare = std::nullopt,
MaybeTestConfigureFunction before_each = std::nullopt, MaybeTestConfigureFunction before_each = std::nullopt,
MaybeTestConfigureFunction after_each = std::nullopt, MaybeTestConfigureFunction after_each = std::nullopt,
bool is_enabled = true) { bool is_enabled = true) {
return make_tuple(suite_name, function_to_test, test_data, compare, before_each, after_each, is_enabled); return make_tuple(suite_name, function_to_test, test_data, Compare, before_each, after_each, is_enabled);
}
/// @brief
/// @tparam TResult The result type of the test.
/// @tparam TInputParams... The types of parameters sent to the test function.
/// @param test_suite A tuple representing the test suite configuration.
template <typename TResult, typename... TInputParams>
TestResults execute_suite(const TestSuite<TResult, TInputParams...>& test_suite) {
return execute_suite<TResult, TInputParams...>(
std::get<0>(test_suite), std::get<1>(test_suite), std::get<2>(test_suite)
// TODO: make this work for the optional parts of the tuple too.
);
}
/// @brief
/// @tparam ...TInputParams
/// @param first
/// @param second
/// @return
template <typename... TInputParams>
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;
}
} }
/// @brief Writes a friendly version of results to the provided stream. /// @brief Writes a friendly version of results to the provided stream.
/// @param os The stream to write to. /// @param os The stream to write to.
/// @param results The TestResults to write. /// @param results The TestResults to write.
void PrintResults(std::ostream& os, TestResults results); void PrintResults(std::ostream& os, TestResults results);
/// @brief
/// @param first
/// @param second
/// @return
MaybeTestConfigureFunction Coalesce(MaybeTestConfigureFunction first, MaybeTestConfigureFunction second);
// TODO: Document this.
// Tuple printer based on code from:
// https://stackoverflow.com/questions/6245735/pretty-print-stdtuple/31116392#58417285
template <typename TChar, typename TTraits, typename... TArgs>
auto& operator<<(std::basic_ostream<TChar, TTraits>& os, std::tuple<TArgs...> const& t) {
std::apply(
[&os](auto&&... args) {
if (sizeof...(TArgs) == 0) {
os << "[]";
return;
}
size_t n = 0;
os << "[ ";
((TinyTest::PrettyPrint(os, args) << (++n != sizeof...(TArgs) ? ", " : "")), ...);
os << " ]";
},
t);
return os;
}
// TODO: Document this.
template <typename TChar, typename TTraits>
auto& operator<<(std::basic_ostream<TChar, TTraits>& os, const void* pointer) {
os << pointer;
}
// TODO: Simplify this.
template <typename TChar,
typename TTraits,
typename TContainer,
typename = std::enable_if_t<
std::is_same_v<decltype(std::declval<TContainer>().begin()), decltype(std::declval<TContainer>().end())>>,
typename = std::enable_if_t<std::is_base_of_v<
std::input_iterator_tag,
typename std::iterator_traits<decltype(std::declval<TContainer>().begin())>::iterator_category>>>
auto& operator<<(std::basic_ostream<TChar, TTraits>& os, TContainer container) {
os << "[ ";
for (auto it = container.begin(); it != container.end(); it++) {
if (it != container.begin()) {
os << ", ";
}
TinyTest::PrettyPrint(os, *it);
}
os << " ]";
return os;
}
// TODO: Document this.
template <typename TChar, typename TTraits, typename TItem>
auto& operator<<(std::basic_ostream<TChar, TTraits>& os, std::initializer_list<TItem> container) {
os << "[ ";
for (auto it = container.begin(); it != container.end(); it++) {
if (it != container.begin()) {
os << ", ";
}
TinyTest::PrettyPrint(os, *it);
}
os << " ]";
return os;
}
// TODO: Document this.
template <typename TChar, typename TTraits, typename TItem>
auto& Compare(std::basic_ostream<TChar, TTraits>& error_message,
std::vector<TItem> expected,
std::vector<TItem> actual) {
if (expected.size() != actual.size()) {
error_message << "size mismatch expected: " << expected.size() << ", actual: " << actual.size();
return error_message;
}
for (size_t index = 0; index < expected.size(); index++) {
if (expected[index] != actual[index]) {
error_message << "vectors differ at index " << index << ", \"" << expected[index] << "\" != \"" << actual[index]
<< "\", expected: \"" << expected << "\", actual: \"" << actual << "\"";
return error_message;
}
}
return error_message;
}
} // End namespace TinyTest } // End namespace TinyTest
#endif // End !defined TEST_H__ #endif // End !defined TEST_H__

File diff suppressed because it is too large Load Diff