非测试代码中的 gtest 断言
gtest assertions in non-test code
我正在开发一个C++库,我正在使用gtest进行单元测试。我想将 ASSERT_* 语句添加到库代码本身,而不仅仅是单元测试代码。我希望这些断言在代码在单元测试下运行时导致单元测试失败,或者如果代码不在联合测试下运行,则转换为常规断言。
像这样:
if(gtest::is_running)
ASSERT_TRUE(...);
else
assert(...);
我怎样才能做到这一点?
即使这在技术上是可行的(我认为不是),我真的不相信让你的生产代码依赖于测试框架是一个好主意。
主要原因是健壮性、关注点分离和解耦:在生产代码中引入特定于测试的条件会使代码不必要地难以理解,并且可能会降低测试套件的可信度(毕竟,您的测试不会强调生产代码将经历的完全相同的路径)。
此外,有一天你可能想要更改测试环境中的某些内容(例如单元测试框架的版本或单元测试框架本身),这种依赖关系可能会迫使你相应地修改生产代码,冒着引入新错误的风险。
如果你想验证的是,当客户端违反函数的前提条件时,你的断言是否真的被触发了(即,如果你想测试你的断言是否正确验证了前提条件),那么这个提议可能与你以及启发它的库彭博的 BDE 相关。
如果这对于您的项目来说不是一个可行的技术,也许您可以考虑采用基于依赖关系反转的策略。最简单的方法是:
- 定义一个抽象类
Verifier
,verify()
采用bool
的抽象成员函数; - 从中派生一个
AssertingVerifier
类(用于生产代码),该类覆盖verify()
并将其参数转发给assert()
。Verifier
和AssertVerifier
都将存在于您的生产代码中; - 在单元测试项目中,定义第二个派生类
GracefulTestVerifier
,该类重写verify()
并将其参数转发给ASSERT_TRUE()
- 或者通过执行您认为最合适的任何操作; - 找出将
Verifier
注入生产代码的最佳方式 - 存在几种可能性,但要知道哪一种最适合需要详细了解您的设计。然后,您将在常规执行环境中注入AssertVerifier
,在测试环境中注入GracefulTestVerifier
。
这样,执行可以从生产代码流向测试框架,而生产代码在物理上依赖于测试框架本身。
您可以使用预处理器指令。
使用 gtest 进行编译时,告诉编译器定义类似"GTEST_ON"的内容,然后在代码中:
#ifdef GTEST_ON
ASSERT_TRUE(...);
#else
assert(...);
#endif
从另一个方向接近这个怎么样?与其改变你的测试行为,不如改变你的断言的行为。
例如,Boost.Assert提供了一个BOOST_ASSERT
宏,默认情况下,该宏的行为与assert
相同。但是,如果定义了BOOST_ENABLE_ASSERT_HANDLER
,则它会查找必须提供的::boost::assertion_failed
函数。您可以将库代码设计为在测试套件之外使用标准断言行为构建,并使用在测试套件内调用 gtest FAIL()
的::boost::assertion_failed
进行构建。
如果你不想使用Boost,那么自己实现类似的东西是微不足道的。
这将需要构建两次库(一次用于测试套件,一次用于常规使用),这可能不太符合您的总体目标。
以下是我最终做的事情,遵循@Josh凯利的建议:
我已经从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集成断言。
我基本上使用了这组源代码。它们非常独立。
要使其正常工作,您必须执行以下步骤:
-
使用从库源代码编译的单元测试,而不是与库文件的链接(这很容易用
cmake
)。 - 将单元测试和基准测试提取到独立项目中,
UNIT_TESTS
定义已为单元测试定义,尚未为基准测试定义。 - 在包含
gtest/gtest.hpp
标头之前,将utility/assert.hpp
包含在单元测试和主项目的主标头中的某个地方。 - 使用
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;
}
}
实用/调试.hpp
#pragma once
namespace utility
{
void debug_break(bool breakCondition = true);
bool is_under_debugger();
}
实用程序/调试.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
}
}
实用/平台.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
- 在gtest.中使用fff.h模拟系统API
- 尝试使用 std::vector<std::thread时出现静态断言失败错误>
- uint_not_usable_without_attribute在业力规则中使用数字生成器时静态断言失败
- C++ 使用增强正则表达式库时断言崩溃
- 如何解决GTest和LibTorch联动冲突
- 如何将GTest与CMake一起使用?遵循谷歌指南时的链接问题
- 从 exe 文件 (Visual Studio ) 启动时调试断言失败
- 在 gtest 中初始化堆栈上的引用变量的隔离错误
- 如何将向量断言到特征矩阵
- OpenCV - Python 断言错误:SAD 算法 - 立体相机视差图计算
- 使用 Google Test 对自定义断言函数进行单元测试
- 断言"id < 0"在Qt ActiveX中失败
- 初始值设定项列表构造和静态断言
- GTest,仅参数化测试用例
- 在 CppUnit 中测试中止断言失败
- 使用 gtest 时"_main already defined"
- C ++ Google test (gtest):如何创建自定义断言和期望?
- gtest 如何记录断言的结果
- 如何在GTEST中否定匹配者断言或期望这一点
- 非测试代码中的 gtest 断言