简介

MiniTest 是一个轻量级 C++ 单元测试框架,提供 断言测试、参数化测试、分组测试、性能测试和 Mock(模拟对象),适用于小型项目的单元测试需求。

本框架的特点:

  • 轻量级:无第三方依赖,适用于嵌入式、CLI 工具等。
  • 易扩展:模块化设计,支持自定义测试。
  • 支持参数化测试:使用 TEST_P 运行多组输入测试。
  • 支持性能测试:基准测试 BENCHMARK_FUNC 测量函数执行时间。
  • 支持 Mock:通过 Mock::SetReturn 轻松模拟依赖。

项目结构

MiniTest/
│── include/                 # 头文件目录
│   ├── TestAssert.hpp       # 断言宏
│   ├── TestFramework.hpp    # 基础测试框架
│   ├── TestParams.hpp       # 参数化测试
│   ├── TestSuite.hpp        # 分组测试
│   ├── TestBenchmark.hpp    # 性能测试
│   ├── TestMock.hpp         # Mock 模拟对象
│── tests/                   # 测试用例
│── CMakeLists.txt           # CMake 构建脚本
│── main.cpp                 # 主测试入口
│── README.md                # 项目文档

环境配置

本项目使用 C++20,通过 CMake 构建。

cmake_minimum_required(VERSION 3.30)
project(MiniTest)

set(CMAKE_CXX_STANDARD 20)

include_directories(include)
add_executable(MiniTest main.cpp)

target_compile_options(MiniTest PRIVATE -finput-charset=UTF-8 -fexec-charset=UTF-8)

编译运行

mkdir build
cd build
cmake ..
make

1. 断言测试

示例代码

TEST(TestBoolean) {
    ASSERT_TRUE(true);
}

TEST(TestFailure) {
    ASSERT_TRUE(false); // 这个测试会失败
}

TEST(TestException) {
    ASSERT_THROW(throw std::runtime_error("error"), std::runtime_error);
}

TEST(TestSubtraction) {
    ASSERT_EQ(5 - 3, 2);
}

完整代码

/**
* ==================================================
 *  @file TestAssert.hpp
 *  @brief 断言宏定义,提供基本的单元测试断言功能
 *  @author mrDarker
 *  @date 2025/03/18
 *  @version 1.0
 *  @copyright Copyright (c) 2025 mrDarker. All Rights Reserved.
 * ==================================================
 */

#ifndef TEST_ASSERT_HPP
#define TEST_ASSERT_HPP

#include <iostream>
#include <stdexcept>

// 断言失败时记录日志
#define ASSERT_FAIL(message) \
do { \
    /**std::ostringstream oss; \
    oss << "Assertion failed: " << message << " at " << __FILE__ << ":" << __LINE__; \
    std::cerr << "[ASSERT FAIL] " << oss.str() << std::endl; \
    TestLogger logger("AssertFailures"); \
    logger.LogMessage(oss.str()); \
    std::cout.flush(); \
    std::cerr.flush(); \
    throw std::runtime_error(oss.str()); \ **/ \
} while (0)

// 断言:检查是否为真
#define ASSERT_TRUE(condition) \
do { \
    if (!(condition)) { \
        ASSERT_FAIL(#condition " is false"); \
    } \
} while (0)

// 断言:检查是否为假
#define ASSERT_FALSE(condition) \
do { \
    if ((condition)) { \
        ASSERT_FAIL(#condition " is true"); \
    } \
} while (0)

// 断言:检查两个值是否相等
#define ASSERT_EQ(expected, actual) \
do { \
    if ((expected) != (actual)) { \
        std::ostringstream oss; \
        oss << #expected " != " #actual " (" << expected << " != " << actual << ")"; \
        ASSERT_FAIL(oss.str()); \
    } \
} while (0)

// 断言:检查两个值是否不相等
#define ASSERT_NE(expected, actual) \
do { \
    if ((expected) == (actual)) { \
        std::ostringstream oss; \
        oss << #expected " == " #actual " (" << expected << " == " << actual << ")"; \
        ASSERT_FAIL(oss.str()); \
    } \
} while (0)

// 断言:检查某个语句是否抛出指定的异常
#define ASSERT_THROW(statement, exception_type) \
do { \
    bool caught = false; \
    try { \
        statement; \
    } catch (const exception_type&) { \
        caught = true; \
    } catch (...) { \
        ASSERT_FAIL("Unexpected exception type thrown"); \
    } \
    if (!caught) { \
        ASSERT_FAIL("Expected exception of type " #exception_type " not thrown"); \
    } \
} while (0)

#endif // TEST_ASSERT_HPP

支持的断言宏

宏说明
ASSERT_TRUE(x)断言 x 为 true
ASSERT_FALSE(x)断言 x 为 false
ASSERT_EQ(a, b)断言 a == b
ASSERT_NE(a, b)断言 a != b
ASSERT_THROW(expr, exception_type)断言 expr 抛出 exception_type

工作原理

  1. ASSERT_* 宏会检查表达式的结果。
  2. 如果断言失败,会抛出 std::runtime_error,使测试失败。
  3. TestFramework::RunAllTests() 负责运行所有注册的测试。

2. 参数化测试

示例代码

std::vector<std::tuple<int, int, int>> additionParams = {
    {1, 2, 3}, {4, 5, 9}, {3, 7, 10}, {6, -2, 4}, {8, 3, 12}
};

TEST_P(TestParamsAddition, additionParams, int a, int b, int expected) {
    ASSERT_EQ(a + b, expected);
}

完整代码

/**
 * ==================================================
 *  @file TestParams.hpp
 *  @brief 参数化测试框架,支持数据集驱动测试
 *  @author mrDarker
 *  @date 2025/03/18
 *  @version 1.1
 *  @copyright Copyright (c) 2025 mrDarker. All Rights Reserved.
 * ==================================================
 */

#ifndef TEST_PARAMS_HPP
#define TEST_PARAMS_HPP

#include <vector>
#include <string>
#include <functional>
#include <iostream>
#include <sstream>
#include <tuple>
#include "TestLogger.hpp"

// 参数化测试的存储结构
class TestParams {
public:
    // 注册参数化测试
    template <typename Func, typename... Args>
    static void RegisterParamTest(const std::string& name, Func func, const std::vector<std::tuple<Args...>>& params) {
        for (const auto& paramSet : params) {
            std::ostringstream oss;
            oss << name << FormatParams(paramSet);
            GetParamTests().push_back({oss.str(), [func, paramSet]() { std::apply(func, paramSet); }});
        }
    }

    // 运行所有参数化测试
    static void RunAllParamTests() {
        TestLogger logger("ParamTests");

        int passed = 0, failed = 0;
        for (const auto&[name, func] : GetParamTests()) {
            std::cout.flush();
            std::cerr.flush();
            std::cout << "[RUNNING] " << name << std::endl;
            try {
                func();  // 无参数调用
                std::cout << "[PASS] " << name << std::endl;
                logger.LogTestResult(name, true);
                ++passed;
            } catch (const std::exception& ex) {
                std::cerr << "[FAIL] " << name << " - " << ex.what() << std::endl;
                logger.LogTestResult(name, false);
                ++failed;
            }
        }

        // 打印测试统计信息
        std::cout << "===========================================" << std::endl;
        std::cout << "Total: " << (passed + failed) << ", Passed: " << passed << ", Failed: " << failed << std::endl;
        std::cout << "===========================================" << std::endl;
        std::cout.flush();
        std::cerr.flush();
    }

private:
    struct ParamTestCase {
        std::string name;
        std::function<void()> func;
    };

    static std::vector<ParamTestCase>& GetParamTests() {
        static std::vector<ParamTestCase> paramTests;
        return paramTests;
    }

    // 格式化参数列表
    template <typename Tuple, size_t... Index>
    static std::string FormatTupleImpl(const Tuple& tuple, std::index_sequence<Index...>) {
        std::ostringstream oss;
        ((oss << (Index == 0 ? "" : ", ") << std::get<Index>(tuple)), ...);
        return "(" + oss.str() + ")";
    }

    template <typename... Args>
    static std::string FormatParams(const std::tuple<Args...>& params) {
        return FormatTupleImpl(params, std::index_sequence_for<Args...>{});
    }
};

// 参数化测试宏
#define TEST_P(test_name, param_data, ...) \
void test_name(__VA_ARGS__); \
namespace { \
    struct Register_##test_name { \
        Register_##test_name() { \
            static const auto _param_data = param_data; \
            TestParams::RegisterParamTest(#test_name, test_name, _param_data); \
        } \
    }; \
    static Register_##test_name g_register_##test_name; \
} \
void test_name(__VA_ARGS__)

#endif // TEST_PARAMS_HPP

工作原理

  1. TEST_P 宏会自动生成测试函数,并注册到 TestParams::RegisterParamTest。
  2. std::tuple<> 存储不同的测试数据。
  3. TestParams::RunAllParamTests() 依次调用测试用例,并使用不同参数执行。

3. 分组测试

示例代码

TEST_SUITE(MathTests, TestSuiteAddition) {
    ASSERT_EQ(2 + 3, 5);
}

TEST_SUITE_F(MySuiteTestFixture, MathTests, TestAddition) {
    ASSERT_EQ(1 + 1, 2);
}

完整代码

/**
 * ==================================================
 *  @file TestSuite.hpp
 *  @brief 测试分组管理,支持运行指定测试套件
 *  @author mrDarker
 *  @date 2025/03/18
 *  @version 1.1
 *  @copyright Copyright (c) 2025 mrDarker. All Rights Reserved.
 * ==================================================
 */

#ifndef TEST_SUITE_HPP
#define TEST_SUITE_HPP

#include <vector>
#include <functional>
#include <unordered_map>
#include <iostream>
#include <ranges>
#include <thread>
#include "TestLogger.hpp"

class TestSuite {
public:
    // 结构体: 测试用例
    struct TestCase {
        std::string name;           // 测试名称
        std::function<void()> func; // 测试执行函数
    };

    /**
     * @brief 注册测试到指定的 Suite
     * @param suite 测试套件名称
     * @param test 测试名称
     * @param func 测试函数
     */
    static void RegisterTest(const std::string& suite, const std::string& test, const std::function<void()> &func) {
        GetSuites()[suite].push_back({test, func});
    }

    /**
     * @brief 打印所有已注册的测试套件
     */
    static void ListSuites() {
        std::cout << "[AVAILABLE TEST SUITES]" << std::endl;
        for (const auto &suite: GetSuites() | std::views::keys) {
            std::cout << "- " << suite << std::endl;
        }
    }

    /**
     * @brief 执行指定测试套件的 Setup
     */
    static void SetupSuite(const std::string& suite) {
        if (auto& setupFuncs = GetSetupFuncs(); setupFuncs.contains(suite)) setupFuncs[suite]();
    }

    /**
     * @brief 执行指定测试套件的 Teardown
     */
    static void TeardownSuite(const std::string& suite) {
        if (auto& teardownFuncs = GetTeardownFuncs(); teardownFuncs.contains(suite)) teardownFuncs[suite]();
    }

    /**
     * @brief 获取所有测试套件的 Setup 函数映射
     * @return std::unordered_map<std::string, std::function<void()>>&
     */
    static std::unordered_map<std::string, std::function<void()>>& GetSetupFuncs() {
        static std::unordered_map<std::string, std::function<void()>> setupFuncs;
        return setupFuncs;
    }

    /**
     * @brief 获取所有测试套件的 Teardown 函数映射
     * @return std::unordered_map<std::string, std::function<void()>>&
     */
    static std::unordered_map<std::string, std::function<void()>>& GetTeardownFuncs() {
        static std::unordered_map<std::string, std::function<void()>> teardownFuncs;
        return teardownFuncs;
    }

    /**
     * @brief 运行指定测试套件,可选过滤单个测试
     * @param suite 测试套件名称
     * @param testFilter 需要执行的具体测试(默认为空,执行整个套件)
     */
    static void RunSuite(const std::string& suite, const std::string& testFilter = "") {
        auto& suites = GetSuites();
        if (!suites.contains(suite)) {
            std::cerr << "[ERROR] Test suite '" << suite << "' not found.\n";
            return;
        }

        auto& tests = suites[suite];

        // 如果 testFilter 为空,执行整个 Suite
        if (testFilter.empty()) {
            std::cout << "[RUNNING SUITE] " << suite << std::endl;
            RunTests(tests, suite);
            return;
        }

        // 查找是否存在该 Test
        auto it = std::ranges::find_if(tests, [&](const TestCase& test) {
            return test.name == testFilter;
        });

        // 如果存在,执行该 Test
        if (it != tests.end()) {
            std::cout << "[RUNNING SINGLE TEST] " << suite << "::" << testFilter << std::endl;
            RunTests({*it}, suite);
        } else {
            std::cerr << "[ERROR] Test '" << testFilter << "' not found in suite '" << suite << "'.\n";
        }
    }

    /**
     * @brief 运行测试列表
     * @param tests 需要执行的测试列表
     * @param suite 所属的测试套件名称
     */
    static void RunTests(const std::vector<TestCase>& tests, const std::string& suite) {
        TestLogger logger(suite);
        int passed = 0, failed = 0;

        // 执行 Setup
        SetupSuite(suite);

        for (const auto& [name, func] : tests) {
            std::cout << "[RUNNING] " << name << std::endl;
            try {
                func();
                std::cout << "[PASS] " << name << std::endl;
                logger.LogTestResult(name, true);
                ++passed;
            } catch (const std::exception& ex) {
                std::cerr << "[FAIL] " << name << " - " << ex.what() << std::endl;
                logger.LogTestResult(name, false);
                ++failed;
            }
        }

        // 执行 Teardown
        TeardownSuite(suite);

        std::cout << "[SUITE] " << suite << " - Passed: " << passed << ", Failed: " << failed << std::endl;
    }

    /**
    * @brief 运行所有测试套件
    */
    static void RunAllSuites() {
        std::vector<std::thread> threads;
        for (const auto &key: GetSuites() | std::views::keys) {
            threads.emplace_back([key]() { RunSuite(key); });
        }
        for (auto& t : threads) {
            if (t.joinable()) t.join();
        }
    }

private:
    /**
    * @brief 获取所有已注册的测试套件
    */
    static std::unordered_map<std::string, std::vector<TestCase>>& GetSuites() {
        static std::unordered_map<std::string, std::vector<TestCase>> suites;
        return suites;
    }
};

// **注册 Setup**
#define TEST_SUITE_SETUP(suite_name, func) \
namespace { struct RegisterSetup_##suite_name { \
    RegisterSetup_##suite_name() { TestSuite::GetSetupFuncs()[#suite_name] = func; } \
}; static RegisterSetup_##suite_name g_registerSetup_##suite_name; }

// **注册 Teardown**
#define TEST_SUITE_TEARDOWN(suite_name, func) \
namespace { struct RegisterTeardown_##suite_name { \
    RegisterTeardown_##suite_name() { TestSuite::GetTeardownFuncs()[#suite_name] = func; } \
}; static RegisterTeardown_##suite_name g_registerTeardown_##suite_name; }

// **注册普通测试**
#define TEST_SUITE(suite_name, test_name) \
void test_name(); \
namespace { \
    struct Register_##test_name { \
        Register_##test_name() { TestSuite::RegisterTest(#suite_name, #test_name, test_name); } \
    }; \
    static Register_##test_name g_register_##test_name; \
} \
void test_name()

// **注册带 Fixture 的测试**
#define TEST_SUITE_F(fixture_name, suite_name, test_name) \
class test_name : public fixture_name { \
public: \
    void Run(); \
}; \
namespace { \
    struct Register_##test_name { \
        Register_##test_name() { \
            TestSuite::RegisterTest(#suite_name, #test_name, []() { \
                test_name instance; \
                instance.Run(); \
            }); \
        } \
    }; \
    static Register_##test_name g_register_##test_name; \
} \
void test_name::Run()

#endif // TEST_SUITE_HPP

工作原理

  1. TEST_SUITE(suite, test) 注册测试到 TestSuite::RegisterTest。
  2. TEST_SUITE_F(fixture, suite, test) 支持使用 SetUp() 和 TearDown()。
  3. TestSuite::RunSuite("MathTests") 运行整个测试套件。

4. 性能测试

示例代码

BENCHMARK_FUNC(MyTestFunction, 5);

void MyTestFunction() {
    std::vector<int> data(100000);
    std::ranges::generate(data.begin(), data.end(), rand);
    std::ranges::sort(data.begin(), data.end());
}

完整代码

/**
 * ==================================================
 *  @file TestBenchmark.hpp
 *  @brief 基准测试框架,支持多次运行、平均时间统计和参数化基准测试
 *  @author mrDarker
 *  @date 2025/03/18
 *  @version 1.2
 *  @copyright Copyright (c) 2025 mrDarker. All Rights Reserved.
 * ==================================================
 */

#ifndef TEST_BENCHMARK_HPP
#define TEST_BENCHMARK_HPP

#include <chrono>
#include <functional>
#include <iostream>
#include <vector>
#include <unordered_map>
#include "TestLogger.hpp"

class TestBenchmark {
public:
    // 普通基准测试(无参数)
    static void RegisterBenchmark(const std::string& name, const std::function<void()>& func, const int iterations) {
        GetBenchmarks()[name] = {func, iterations};
    }

    // 参数化基准测试(有参数)
    template <typename Func, typename... Args>
    static void RegisterBenchmark(const std::string& name, Func func, int iterations, Args... args) {
        auto wrapper = [func, args...]() { func(args...); };
        GetBenchmarks()[name] = {wrapper, iterations};
    }

    // 运行所有基准测试
    static void RunAllBenchmarks() {
        RunFilteredBenchmarks([](const std::string&) { return true; }, "AllBenchmarks");
    }

    // 运行指定的基准测试
    static void RunBenchmark(const std::string& name) {
        RunFilteredBenchmarks([name](const std::string& testName) {
            return testName == name;
        }, "Benchmark_" + name);
    }

    // 单独测试某个函数的平均执行时间
    template <typename ReturnType, typename... Args>
    static void BenchmarkFunction(const std::string& name, ReturnType (*func)(Args...), const int iterations, Args... args) {
        std::cout << "[BENCHMARK] Running " << name << " for " << iterations << " iterations..." << std::endl;
        std::vector<double> times;

        for (int i = 0; i < iterations; ++i) {
            std::cout.flush();
            std::cerr.flush();

            auto start = std::chrono::high_resolution_clock::now();
            if constexpr (sizeof...(Args) > 0) {
                func(args...);  // 如果有参数,调用带参数的函数
            } else {
                func();         // 如果无参数,调用无参数的函数
            }
            auto end = std::chrono::high_resolution_clock::now();
            std::chrono::duration<double, std::milli> duration = end - start;
            times.push_back(duration.count());
        }

        double totalTime = 0;
        for (const double t : times) totalTime += t;
        const double avgTime = totalTime / iterations;

        std::cout << "[BENCHMARK] " << name << " Avg Time: " << avgTime << " ms" << std::endl;
    }

private:
    struct BenchmarkCase {
        std::function<void()> func;
        int iterations{};
    };

    static std::unordered_map<std::string, BenchmarkCase>& GetBenchmarks() {
        static std::unordered_map<std::string, BenchmarkCase> benchmarks;
        return benchmarks;
    }

    // 通用性能测试执行
    static void RunFilteredBenchmarks(const std::function<bool(const std::string&)>& filter, const std::string& logCategory) {
        TestLogger logger(logCategory);

        for (const auto& [fst, snd] : GetBenchmarks()) {
            if (!filter(fst)) continue;

            const auto& [func, iterations] = snd;
            std::cout << "[BENCHMARK] Running " << fst << " for " << iterations << " iterations..." << std::endl;

            std::vector<double> times;
            for (int i = 0; i < iterations; ++i) {
                std::cout.flush();
                std::cerr.flush();

                auto start = std::chrono::high_resolution_clock::now();
                func();
                auto end = std::chrono::high_resolution_clock::now();
                std::chrono::duration<double, std::milli> duration = end - start;
                times.push_back(duration.count());
            }

            double totalTime = 0;
            for (const double t : times) totalTime += t;
            const double avgTime = totalTime / iterations;

            std::cout << "[BENCHMARK] " << fst << " Avg Time: " << avgTime << " ms" << std::endl;
            logger.LogMessage("[BENCHMARK] " + fst + " Avg Time: " + std::to_string(avgTime) + " ms");
        }
    }
};

// 普通基准测试(无参数)
#define BENCHMARK(test_name, iterations) \
void test_name(); \
namespace { \
    struct Register_##test_name { \
        Register_##test_name() { \
            TestBenchmark::RegisterBenchmark(#test_name, test_name, iterations); \
        } \
    }; \
    static Register_##test_name g_register_##test_name; \
} \
void test_name()

// 指定函数基准测试
#define BENCHMARK_FUNC(func, iterations, ...) \
namespace { \
    struct Register_##func { \
        Register_##func() { \
            TestBenchmark::BenchmarkFunction(#func, func, iterations, ##__VA_ARGS__); \
        } \
    }; \
    static Register_##func g_register_##func; \
}

#endif // TEST_BENCHMARK_HPP

工作原理

  1. BENCHMARK_FUNC 注册 MyTestFunction,并运行 5 次。
  2. TestBenchmark::BenchmarkFunction() 计算平均执行时间。

5. Mock(模拟对象)

示例代码

MOCK_METHOD(int, GetRandomNumber, ());

TEST(TestMockExample) {
    Mock::SetReturn(GetRandomNumber, 42);
    ASSERT_EQ(GetRandomNumber(), 42);
}

完整代码

/**
 * ==================================================
 *  @file TestMock.hpp
 *  @brief 模拟对象框架,支持 Mock 方法和返回值设置
 *  @author mrDarker
 *  @date 2025/03/18
 *  @version 1.0
 *  @copyright Copyright (c) 2025 mrDarker. All Rights Reserved.
 * ==================================================
 */

#ifndef TEST_MOCK_HPP
#define TEST_MOCK_HPP

#include <unordered_map>
#include <functional>
#include <typeindex>
#include <map>

class Mock {
public:
    // 设置 Mock 方法的返回值
    template <typename ReturnType>
    static void SetReturn(ReturnType (*func)(), ReturnType returnValue) {
        auto wrapper = [returnValue]() -> ReturnType { return returnValue; };
        GetMockFunctions<ReturnType>()[reinterpret_cast<void*>(func)] = wrapper;
    }

    // 调用 Mock 方法
    template <typename ReturnType>
    static ReturnType Invoke(ReturnType (*func)()) {
        auto& mockMap = GetMockFunctions<ReturnType>();
        auto it = mockMap.find(reinterpret_cast<void*>(func));
        if (it != mockMap.end()) {
            return it->second();
        }
        // std::cerr << "[MOCK ERROR] Function '" << typeid(func).name() << "' not mocked!\n";
        return ReturnType();  // 默认返回 0 / 空值
    }

    // 清除 Mock
    template <typename ReturnType>
    static void Reset() {
        GetMockFunctions<ReturnType>().clear();
    }

    // 清空所有 Mock 方法
    static void ResetAll() {
        mockStorage().clear();
    }

private:
    // 存储不同类型的 Mock 方法
    template <typename ReturnType>
    static std::unordered_map<void*, std::function<ReturnType()>>& GetMockFunctions() {
        static std::unordered_map<void*, std::function<ReturnType()>> mockFunctions;
        return mockFunctions;
    }

    static std::map<std::type_index, void*>& mockStorage() {
        static std::map<std::type_index, void*> storage;
        return storage;
    }
};

// MOCK_METHOD 宏
#define MOCK_METHOD(returnType, functionName, params) \
    returnType functionName params; \
    namespace { \
        struct RegisterMock_##functionName { \
            RegisterMock_##functionName() { \
                Mock::SetReturn(functionName, returnType()); \
            } \
        }; \
        static RegisterMock_##functionName g_register_mock_##functionName; \
    } \
    returnType functionName params { return Mock::Invoke(functionName); }

#endif // TEST_MOCK_HPP

工作原理

  1. MOCK_METHOD 定义 Mock 方法。
  2. Mock::SetReturn(func, value) 设置返回值。
  3. Mock::Reset() 清除 Mock 方法。

6. 运行测试

int main() {
    TestFramework::RunAllTests();
    TestParams::RunAllParamTests();
    TestBenchmark::RunAllBenchmarks();
    TestSuite::RunAllSuites();
    return 0;
}

总结

MiniTest 提供 断言、参数化、分组、性能测试和 Mock,适用于 C++ 小型项目单元测试。

✅ 轻量级,无第三方依赖
✅ 支持参数化测试
✅ 支持性能测试
✅ 支持 Mock
✅ 易扩展

这篇博客整理了完整的代码和实现原理。如果有任何改进建议,请告诉我!🚀

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐