单元测试应用程序到硬件的接口——模拟与否
Unit Testing application interface to hardware - to mock or not
我很好奇人们对与硬件接口的软件端应用程序应该以何种方式进行单元测试的意见。
例如,软件应用程序 "Connection" 的主要 class 将构建 USB 设备的句柄。
我想测试 "Connection" class 基本功能,比如 "OpenConnection" 会尝试连接到 USB 硬件设备。
到目前为止,我已经构建了一个 MOCK 硬件设备,并在我的连接中包含了一个编译器标志 class,因此如果它内置于单元测试模式,它将使用模拟对象,否则它将使用实际的硬件接口。
参见下面的示例
class TConnection
{
public:
static TConnection* GetConnection();
static void Shutdown();
bool DidInitialise();
bool Write(uint8_t* _pu8_buffer);
bool Read(uint8_t* _pu8_buffer);
protected:
TConnection();
virtual ~TConnection();
bool init();
private:
static TConnection* mp_padConnection;
static bool mb_DidInitialise;
#ifdef _UNIT_TEST_BUILD
static mock_device* mp_handle;
#else
static device* mp_handle;
#endif
};
然后在源文件中包含类似
的内容
#include "connection.h"
#ifdef _UNIT_TEST_BUILD
mock_device* TConnection::mp_handle = nullptr;
#else
device* TConnection::mp_handle = nullptr;
#endif // _UNIT_TEST_BUILD
TConnection::TConnection()
{
...
init();
...
}
bool TConnection::init()
{
mp_handle = hid_open( _VENDOR_ID, _PRODUCT_ID, nullptr );
if (mp_hidHandle == nullptr) {
return false;
}
if (hid_set_nonblocking(mp_hidHandle, _DISABLE_NB) == _ERROR_CODE) {
return false;
}
return true;
}
我唯一真正不喜欢我的代码的地方是我的实际连接 class 包含测试代码。我更希望它们是分开的。
话虽如此,我也不同意完全为了单元测试的目的而编写一个全新的模拟连接 class,这让我觉得我只是在编写一些旨在按预期工作的东西。
所以我问,什么是测试这样一个更好的方法 class
提前感谢您的宝贵时间和建议
就我个人而言,我会将模拟作为单独的 class,或者作为测试代码的一部分。为了区分模拟库和实际库,我会在构建脚本中进行更改,我假设这将包括测试文件(和模拟)或 链接到库。
创建一个单独的 class 并非白费力气。它应该按预期运行,但这可以简化为测试所需的最低限度。更有趣的是让 class 生成错误事件,以确保您的代码正确处理这些事件。替代方法有时是等待错误发生,我不推荐这样做。
关于该主题的两本强烈推荐的书籍:
- Test-Driven Development for Embedded C:第 2 部分很好地概述了测试替身(模拟是一个例子)以及如何使用它们。
- Modern C++ Programming with Test-Driven Development:测试替身较少,但更适合 C++。
您可以通过使用依赖注入来避免向您的 class 添加测试代码。创建接口 IDevice
并使 class Device
实现该接口。然后,在 class TConnection
中,使用指向此接口的指针而不是 Device
类型的成员。同时创建一个辅助方法,允许您设置新设备,例如:
void setDevice(IDevice *device);
现在,对于您的生产代码,只需使用 class Device
的实例,而在您的测试代码中,使用 setDevice
将设备的实现与模拟对象交换。这个模拟对象将是 class MockDevice
的一个实例,它也将实现接口 IDevice
。这样您就可以更改测试中的实现并使用模拟 class。由于您已经在使用 gtest,我建议您不要自己编写模拟 class,而是使用 C++ 模拟框架 gmock(与 gtest 完全兼容)。这样,您还需要创建一个单独的 class 但几乎所有内容都将由模拟框架处理。您需要做的就是定义模拟方法。创建一个额外的接口和 mock class 乍一看似乎有点过分,但在长期 运行 中肯定会得到回报。如果你想对代码进行任何严肃的测试驱动,学习使用接口、依赖注入和 mock classes 是必不可少的。查看文档了解更多详情:
https://github.com/google/googlemock/blob/master/googlemock/docs/CheatSheet.md
我很好奇人们对与硬件接口的软件端应用程序应该以何种方式进行单元测试的意见。
例如,软件应用程序 "Connection" 的主要 class 将构建 USB 设备的句柄。
我想测试 "Connection" class 基本功能,比如 "OpenConnection" 会尝试连接到 USB 硬件设备。
到目前为止,我已经构建了一个 MOCK 硬件设备,并在我的连接中包含了一个编译器标志 class,因此如果它内置于单元测试模式,它将使用模拟对象,否则它将使用实际的硬件接口。
参见下面的示例
class TConnection
{
public:
static TConnection* GetConnection();
static void Shutdown();
bool DidInitialise();
bool Write(uint8_t* _pu8_buffer);
bool Read(uint8_t* _pu8_buffer);
protected:
TConnection();
virtual ~TConnection();
bool init();
private:
static TConnection* mp_padConnection;
static bool mb_DidInitialise;
#ifdef _UNIT_TEST_BUILD
static mock_device* mp_handle;
#else
static device* mp_handle;
#endif
};
然后在源文件中包含类似
的内容#include "connection.h"
#ifdef _UNIT_TEST_BUILD
mock_device* TConnection::mp_handle = nullptr;
#else
device* TConnection::mp_handle = nullptr;
#endif // _UNIT_TEST_BUILD
TConnection::TConnection()
{
...
init();
...
}
bool TConnection::init()
{
mp_handle = hid_open( _VENDOR_ID, _PRODUCT_ID, nullptr );
if (mp_hidHandle == nullptr) {
return false;
}
if (hid_set_nonblocking(mp_hidHandle, _DISABLE_NB) == _ERROR_CODE) {
return false;
}
return true;
}
我唯一真正不喜欢我的代码的地方是我的实际连接 class 包含测试代码。我更希望它们是分开的。
话虽如此,我也不同意完全为了单元测试的目的而编写一个全新的模拟连接 class,这让我觉得我只是在编写一些旨在按预期工作的东西。
所以我问,什么是测试这样一个更好的方法 class
提前感谢您的宝贵时间和建议
就我个人而言,我会将模拟作为单独的 class,或者作为测试代码的一部分。为了区分模拟库和实际库,我会在构建脚本中进行更改,我假设这将包括测试文件(和模拟)或 链接到库。
创建一个单独的 class 并非白费力气。它应该按预期运行,但这可以简化为测试所需的最低限度。更有趣的是让 class 生成错误事件,以确保您的代码正确处理这些事件。替代方法有时是等待错误发生,我不推荐这样做。
关于该主题的两本强烈推荐的书籍:
- Test-Driven Development for Embedded C:第 2 部分很好地概述了测试替身(模拟是一个例子)以及如何使用它们。
- Modern C++ Programming with Test-Driven Development:测试替身较少,但更适合 C++。
您可以通过使用依赖注入来避免向您的 class 添加测试代码。创建接口 IDevice
并使 class Device
实现该接口。然后,在 class TConnection
中,使用指向此接口的指针而不是 Device
类型的成员。同时创建一个辅助方法,允许您设置新设备,例如:
void setDevice(IDevice *device);
现在,对于您的生产代码,只需使用 class Device
的实例,而在您的测试代码中,使用 setDevice
将设备的实现与模拟对象交换。这个模拟对象将是 class MockDevice
的一个实例,它也将实现接口 IDevice
。这样您就可以更改测试中的实现并使用模拟 class。由于您已经在使用 gtest,我建议您不要自己编写模拟 class,而是使用 C++ 模拟框架 gmock(与 gtest 完全兼容)。这样,您还需要创建一个单独的 class 但几乎所有内容都将由模拟框架处理。您需要做的就是定义模拟方法。创建一个额外的接口和 mock class 乍一看似乎有点过分,但在长期 运行 中肯定会得到回报。如果你想对代码进行任何严肃的测试驱动,学习使用接口、依赖注入和 mock classes 是必不可少的。查看文档了解更多详情:
https://github.com/google/googlemock/blob/master/googlemock/docs/CheatSheet.md