非测试代码中的 gtest 断言

gtest assertions in non-test code

我正在开发 C++ 库,并且正在使用 gtest 进行单元测试。我想将 ASSERT_* 语句添加到库代码本身,而不仅仅是单元测试代码。如果代码在单元测试下 运行,我希望这些 ASSERTions 导致单元测试失败,或者如果代码在联合测试下不 运行ning 则变成常规断言。

类似于:

if(gtest::is_running)
    ASSERT_TRUE(...);
else
    assert(...);

我怎样才能做到这一点?

即使这在技术上可行(我认为不可能),我真的不认为让您的生产代码依赖于测试框架是个好主意。

主要原因是健壮性、关注点分离和解耦:在生产代码中引入特定于测试的条件会使代码不必要地更难理解,并且可能会降低测试套件的可信度(毕竟,您的测试赢了'强调您的生产代码将通过的完全相同的路径)。

此外,有一天您可能想要更改测试环境中的某些内容(例如单元测试框架的版本或单元测试框架本身),并且这种依赖性可能会迫使您相应地修改生产代码,冒着引入新错误的风险。

如果您要验证的是当客户端违反函数的先决条件时您的断言实际上会触发(即,如果您想测试先决条件是否已被您的断言正确验证),那么 this proposal may be relevant to you as well as the library which inspired it, Bloomberg's BDE.

如果这不是您项目的可行技术,也许您可​​以考虑采用基于依赖倒置的策略。最简单的方法是:

  1. 定义一个抽象 class Verifier,抽象成员函数 verify() 接受 bool;
  2. 从中派生一个 AssertingVerifier class(用于生产代码),覆盖 verify() 并将其参数转发给 assert()VerifierAssertVerifier 都将存在于您的生产代码中;
  3. 在您的单元测试项目中,定义第二个派生的 class、GracefulTestVerifier,它会覆盖 verify() 并将其参数转发给 ASSERT_TRUE() - 或者通过执行您的任何操作认为最合适;
  4. 找出将 Verifier 注入您的生产代码的最佳方法 - 存在多种可能性,但判断哪一种最适合需要您对设计有详细的了解。然后,您将在常规执行环境中注入 AssertVerifier,在测试环境中注入 GracefulTestVerifier

这样,执行可以从生产代码流向测试框架,而您的生产代码在物理上不依赖于测试框架本身。

从另一个方向接近这个怎么样?不要改变你的 gtest 行为,改变你的断言行为。

例如,

Boost.Assert 提供了一个 BOOST_ASSERT 宏,默认情况下,它的行为与 assert 相同。但是,如果定义了 BOOST_ENABLE_ASSERT_HANDLER,那么它会查找您必须提供的 ::boost::assertion_failed 函数。您可以将库代码设计为在测试套件外使用标准断言行为构建,并在测试套件内使用调用 gtest FAIL()::boost::assertion_failed

如果您不想使用 Boost,您自己实现类似的东西会很简单。

这需要两次构建您的库(一次用于测试套件,一次用于常规使用),这可能不符合您的总体目标。

您可以使用预处理器指令。

当用 gtest 编译时,告诉你的编译器定义类似 "GTEST_ON" 的东西,然后在你的代码中:

#ifdef GTEST_ON
    ASSERT_TRUE(...);
#else
    assert(...);
#endif

这是我按照@Josh Kelley 的建议最终做的事情:

我已经从 assert 切换到 BOOST_ASSERT。 我没有包含 boost/assert.hpp,而是添加了我自己的 assert.hpp 文件,其中包含 Boost 文件,定义了 BOOST_ENABLE_ASSERT_HANDLER 和一个 BOOST_ASSERT_HANDLER 函数指针(指向与 Boost 完全相同的类型断言处理程序)。

我还包含了我自己的 Boost 断言处理程序 (::boost::assertion_failed),它将断言信息输出到 std::cerr 并调用 BOOST_ASSERT_HANDLER 指向的函数(如果存在)。如果没有,它只是 assert(false)s.

在我的测试主程序中,我将 BOOST_ASSERT_HANDLER 指向一个简单调用 EXPECT_FALSE(true) 的函数。

就是这样。现在我可以在 gtest 下不 运行 时使用普通断言,而在 gtest 下 运行 时我可以使用 gtest 集成断言。

这些资源我基本上都用过。它们非常独立。

要使其正常工作,您必须执行以下步骤:

  1. 使用从库源代码编译的单元测试而不是与库文件链接(使用cmake很容易做到)。
  2. 将单元测试和基准测试提取到具有 UNIT_TESTS 定义的独立项目中,已为单元测试定义,而没有为基准测试定义。
  3. gtest/gtest.hpp header 包含之前,在单元测试和主项目的主要 header 中包含 utility/assert.hpp 某处。
  4. 使用 ASSERT_TRUE/ASSERT_EQ/etc 代替 assert

注意:在基准测试的情况下,您不应定义 UNIT_TESTS 定义,否则断言定义会减慢执行速度。

utility/assert.hpp

UPD1

  • 修复断言表达式求值,只求一次

'

#pragma once

#include "debug.hpp"

#ifdef UNIT_TESTS
#include <gtest/gtest.h>
#endif

#include <cassert>

#define ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(exp, precondition) \
    if (!(precondition)); else if(!!(exp)); else ::utility::debug_break()

#ifdef GTEST_FAIL

#ifdef _MSC_VER
    #if _MSC_VER < 1600 // < MSVC++ 10 (Visual Studio 2010)
        #error lambda is not supported
    #endif
#else
    #if __cplusplus < 201103L
        #error lambda is not supported
    #endif
#endif

// TIPS:
//  * all lambdas captured by reference because of the error in the MSVC 2015:
//    `error C3493 : '...' cannot be implicitly captured because no default capture mode has been specified`
//  * if debugger is attached but `::testing::GTEST_FLAG(break_on_failure)` has not been setted,
//    then an assertion does a post break.

// gtest asserts rebind with the `void` error workaround (C++11 and higher is required)
#undef ASSERT_TRUE
#define ASSERT_TRUE(condition) [&]() -> void { \
        const bool is_success = ::utility::is_true(condition); \
        const bool break_on_failure = ::testing::GTEST_FLAG(break_on_failure); \
        if (break_on_failure) { \
            GTEST_TEST_BOOLEAN_(is_success, #condition, false, true, GTEST_FATAL_FAILURE_); \
        } else { \
            GTEST_TEST_BOOLEAN_(is_success, #condition, false, true, GTEST_NONFATAL_FAILURE_); \
        } \
        ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(is_success, !break_on_failure); \
    }()
#undef ASSERT_FALSE
#define ASSERT_FALSE(condition) [&]() -> void { \
        const bool is_success = ::utility::is_false(condition); \
        const bool break_on_failure = ::testing::GTEST_FLAG(break_on_failure); \
        if (break_on_failure) { \
            GTEST_TEST_BOOLEAN_(is_success, #condition, true, false, GTEST_FATAL_FAILURE_); \
        } else { \
            GTEST_TEST_BOOLEAN_(is_success, #condition, true, false, GTEST_NONFATAL_FAILURE_); \
        } \
        ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(is_success, !break_on_failure); \
    }()

#if !GTEST_DONT_DEFINE_ASSERT_EQ
#undef ASSERT_EQ
#define ASSERT_EQ(val1, val2) [&]() -> void { \
        const ::testing::AssertionResult exp_value = ::testing::internal::EqHelper<GTEST_IS_NULL_LITERAL_(val1)>::Compare(#val1, #val2, val1, val2); \
        const bool break_on_failure = ::testing::GTEST_FLAG(break_on_failure); \
        if (break_on_failure) { \
            GTEST_ASSERT_(exp_value, GTEST_FATAL_FAILURE_); \
        } else { \
            GTEST_ASSERT_(exp_value, GTEST_NONFATAL_FAILURE_); \
        } \
        ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(exp_value, !break_on_failure); \
    }()
#endif

#if !GTEST_DONT_DEFINE_ASSERT_NE
#undef ASSERT_NE
#define ASSERT_NE(val1, val2) [&]() -> void { \
        const ::testing::AssertionResult exp_value = ::testing::internal::CmpHelperNE(#val1, #val2, val1, val2); \
        const bool break_on_failure = ::testing::GTEST_FLAG(break_on_failure); \
        if (break_on_failure) { \
            GTEST_ASSERT_(exp_value, GTEST_FATAL_FAILURE_); \
        } else { \
            GTEST_ASSERT_(exp_value, GTEST_NONFATAL_FAILURE_); \
        } \
        ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(exp_value, !break_on_failure); \
    }()
#endif

#if !GTEST_DONT_DEFINE_ASSERT_LE
#undef ASSERT_LE
#define ASSERT_LE(val1, val2) [&]() -> void { \
        const ::testing::AssertionResult exp_value = ::testing::internal::CmpHelperLE(#val1, #val2, val1, val2); \
        const bool break_on_failure = ::testing::GTEST_FLAG(break_on_failure); \
        if (break_on_failure) { \
            GTEST_ASSERT_(exp_value, GTEST_FATAL_FAILURE_); \
        } else { \
            GTEST_ASSERT_(exp_value, GTEST_NONFATAL_FAILURE_); \
        } \
        ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(exp_value, !break_on_failure); \
    }()
#endif

#if !GTEST_DONT_DEFINE_ASSERT_LT
#undef ASSERT_LT
#define ASSERT_LT(val1, val2) [&]() -> void { \
        const ::testing::AssertionResult exp_value = ::testing::internal::CmpHelperLT(#val1, #val2, val1, val2); \
        const bool break_on_failure = ::testing::GTEST_FLAG(break_on_failure); \
        if (break_on_failure) { \
            GTEST_ASSERT_(exp_value, GTEST_FATAL_FAILURE_); \
        } else { \
            GTEST_ASSERT_(exp_value, GTEST_NONFATAL_FAILURE_); \
        } \
        ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(exp_value, !break_on_failure); \
    }()
#endif

#if !GTEST_DONT_DEFINE_ASSERT_GE
#undef ASSERT_GE
#define ASSERT_GE(val1, val2) [&]() -> void { \
        const ::testing::AssertionResult exp_value = ::testing::internal::CmpHelperGE(#val1, #val2, val1, val2); \
        const bool break_on_failure = ::testing::GTEST_FLAG(break_on_failure); \
        if (break_on_failure) { \
            GTEST_ASSERT_(exp_value, GTEST_FATAL_FAILURE_); \
        } else { \
            GTEST_ASSERT_(exp_value, GTEST_NONFATAL_FAILURE_); \
        } \
        ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(exp_value, !break_on_failure); \
    }()
#endif

#if !GTEST_DONT_DEFINE_ASSERT_GT
#undef ASSERT_GT
#define ASSERT_GT(val1, val2) [&]() -> void { \
        const ::testing::AssertionResult exp_value = ::testing::internal::CmpHelperGT(#val1, #val2, val1, val2); \
        const bool break_on_failure = ::testing::GTEST_FLAG(break_on_failure); \
        if (break_on_failure) { \
            GTEST_ASSERT_(exp_value, GTEST_FATAL_FAILURE_); \
        } else { \
            GTEST_ASSERT_(exp_value, GTEST_NONFATAL_FAILURE_); \
        } \
        ASSERT_FAIL_BREAK_ON_ATTACHED_DEBUGGER(exp_value, !break_on_failure); \
    }()
#endif

#define ASSERT(x) ASSERT_TRUE(x)

#else

#ifndef ASSERT_IMPL
#define ASSERT_IMPL(exp) assert(exp)
#endif

#ifdef _DEBUG

#define ASSERT_TRUE(exp) ASSERT_IMPL(exp)
#define ASSERT_FALSE(exp) ASSERT_IMPL(!(exp))

#define ASSERT_EQ(v1, v2) ASSERT_IMPL((v1) == (v2))
#define ASSERT_NE(v1, v2) ASSERT_IMPL((v1) != (v2)))
#define ASSERT_LE(v1, v2) ASSERT_IMPL((v1) <= (v2))
#define ASSERT_LT(v1, v2) ASSERT_IMPL((v1) < (v2))
#define ASSERT_GE(v1, v2) ASSERT_IMPL((v1) >= (v2))
#define ASSERT_GT(v1, v2) ASSERT_IMPL((v1) > (v2))

#define ASSERT(exp) ASSERT_IMPL(exp)

#else

#define ASSERT_TRUE(exp) (::utility::is_true(exp), (void)0)
#define ASSERT_FALSE(exp) (::utility::is_false(exp), (void)0))

#define ASSERT_EQ(v1, v2) (::utility::is_equal(v1, v2), (void)0)
#define ASSERT_NE(v1, v2) (::utility::is_not_equal(v1, v2), (void)0)
#define ASSERT_LE(v1, v2) (::utility::is_less_or_equal(v1, v2), (void)0)
#define ASSERT_LT(v1, v2) (::utility::is_less(v1, v2), (void)0)
#define ASSERT_GE(v1, v2) (::utility::is_greater_or_equal(v1, v2), (void)0)
#define ASSERT_GT(v1, v2) (::utility::is_greater(v1, v2), (void)0)

#define ASSERT(exp) ::utility::is_true(exp)

#endif

#endif

namespace utility
{
    // TIPS:
    // * to capture parameters by reference in macro definitions for single evaluation
    // * to suppress `unused variable` warnings like: `warning C4101: '...': unreferenced local variable`
    template<typename T>
    inline bool is_true(const T & v)
    {
        return !!v; // to avoid warnings of truncation to bool
    }

    template<typename T>
    inline bool is_false(const T & v)
    {
        return !v; // to avoid warnings of truncation to bool
    }

    template<typename T1, typename T2>
    inline bool is_equal(const T1 & v1, const T2 & v2)
    {
        return v1 == v2;
    }

    template<typename T1, typename T2>
    inline bool is_not_equal(const T1 & v1, const T2 & v2)
    {
        return v1 != v2;
    }

    template<typename T1, typename T2>
    inline bool is_less_or_equal(const T1 & v1, const T2 & v2)
    {
        return v1 <= v2;
    }

    template<typename T1, typename T2>
    inline bool is_less(const T1 & v1, const T2 & v2)
    {
        return v1 < v2;
    }

    template<typename T1, typename T2>
    inline bool is_greater_or_equal(const T1 & v1, const T2 & v2)
    {
        return v1 >= v2;
    }

    template<typename T1, typename T2>
    inline bool is_greater(const T1 & v1, const T2 & v2)
    {
        return v1 > v2;
    }
}

utility/debug.hpp

#pragma once

namespace utility
{
    void debug_break(bool breakCondition = true);
    bool is_under_debugger();
}

utility/debug.cpp

#include "debug.hpp"
#include "platform.hpp"

#if defined(UTILITY_PLATFORM_WINDOWS)
#include <windows.h>
#include <intrin.h>
#elif defined(UTILITY_PLATFORM_POSIX)
#include <sys/ptrace.h>
#include <signal.h>
static void signal_handler(int) { }
#else
#error is_under_debugger is not supported for this platform
#endif


namespace utility {

void debug_break(bool breakCondition)
{
    // avoid signal if not under debugger
    if (breakCondition && is_under_debugger()) {
#if defined(UTILITY_COMPILER_CXX_MSC)
        __debugbreak(); // won't require debug symbols to show the call stack, when the DebugBreak() will require system debug symbols to show the call stack correctly
#elif defined(UTILITY_PLATFORM_POSIX)
        signal(SIGTRAP, signal_handler);
#else
#error debug_break is not supported for this platform
#endif
    }
}

bool is_under_debugger()
{
#if defined(UTILITY_PLATFORM_WINDOWS)
    return !!::IsDebuggerPresent();
#elif defined(UTILITY_PLATFORM_POSIX)
    return ptrace(PTRACE_TRACEME, 0, NULL, 0) == -1;
#endif
}

}

utility/platform.hpp

#pragma once

// linux, also other platforms (Hurd etc) that use GLIBC, should these really have their own config headers though?
#if defined(linux) || defined(__linux) || defined(__linux__) || defined(__GNU__) || defined(__GLIBC__)
#  define UTILITY_PLATFORM_LINUX
#  define UTILITY_PLATFORM_POSIX
#  if defined(__mcbc__)
#     define UTILITY_PLATFORM_MCBC
#     define UTILITY_PLATFORM_SHORT_NAME "MCBC"
#  elif defined( __astra_linux__ )
#     define UTILITY_PLATFORM_ASTRA_LINUX
#     define UTILITY_PLATFORM_SHORT_NAME "Astra Linux"
#  else
#     define UTILITY_PLATFORM_SHORT_NAME "Linux"
#  endif
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) // BSD:
#  define UTILITY_PLATFORM_BSD
#  define UTILITY_PLATFORM_POSIX
#  define UTILITY_PLATFORM_SHORT_NAME "BSD"
#elif defined(sun) || defined(__sun) // solaris:
#  define UTILITY_PLATFORM_SOLARIS
#  define UTILITY_PLATFORM_POSIX
#  define UTILITY_PLATFORM_SHORT_NAME "Solaris"
#elif defined(__CYGWIN__) // cygwin is not win32:
#  define UTILITY_PLATFORM_CYGWIN
#  define UTILITY_PLATFORM_POSIX
#  define UTILITY_PLATFORM_SHORT_NAME "Cygwin"
#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) // win32:
#  define UTILITY_PLATFORM_WINDOWS
#  define UTILITY_PLATFORM_SHORT_NAME "Windows"
#  if defined(__MINGW32__)  //  Get the information about the MinGW runtime, i.e. __MINGW32_*VERSION.
#     include <_mingw.h>
#  endif
#elif defined(macintosh) || defined(__APPLE__) || defined(__APPLE_CC__) // MacOS
#  define UTILITY_PLATFORM_APPLE
#  define UTILITY_PLATFORM_POSIX
#  define UTILITY_PLATFORM_SHORT_NAME "MacOS"
#elif defined(__QNXNTO__)  // QNX:
#  define UTILITY_PLATFORM_QNIX
#  define UTILITY_PLATFORM_POSIX
#  define UTILITY_PLATFORM_SHORT_NAME "QNX"
#elif defined(unix) || defined(__unix) || defined(_XOPEN_SOURCE) || defined(_POSIX_SOURCE)
#  define UTILITY_PLATFORM_UNIX
#  define UTILITY_PLATFORM_POSIX
#  define UTILITY_PLATFORM_SHORT_NAME "Unix"
#else
#   error Unknown platform
#endif

#if defined(__GNUC__)
#   define UTILITY_COMPILER_CXX_GCC
#   define UTILITY_COMPILER_CXX "gcc"
#   define UTILITY_COMPILER_CXX_VERSION __GNUC__
#   if __GNUC__ < 4
#     error "Unsuported gcc version"
#   endif
#elif defined(_MSC_VER)
#   define UTILITY_COMPILER_CXX_MSC
#   define UTILITY_COMPILER_CXX "MS VisualC"
#   define UTILITY_COMPILER_CXX_VERSION _MSC_VER
#else
#   error "Unknown compiler"
#endif