Background
我正在从事一个与各种相机及其独特 API 接口的项目。所有相机都抽象到一个接口中,特定于相机的 API 调用包含在它们自己的实现类中。
问题解释
我正在尝试测试一种工厂方法,该方法解析一种配置形式,并返回指向新创建的相机的指针。下面是它的外观的高级视图:
// camerafactory.cpp
Camera* make_camera(Config& config) {
switch (config.type) {
case CameraTypes.A : {
int paramInt = config.paramInt;
return new CameraA(paramInt);
}
case CameraTypes.B : {
string paramString = config.paramString;
return new CameraB(paramString);
}
case CameraTypes.C : {
float param1 = config.param1;
float param2 = config.param2;
return new CameraC(param1, param2);
}
default: {
throw std::invalid_argument("Invalid configuration!");
}
}
}
我想测试这些构造函数是否正确调用,但这需要将相机物理连接到计算机,以允许相机构造函数中的相机 API 工作。这对于某些测试情况确实有意义,即在循环中演示硬件,但是在我的特定情况下,一次连接所有类型的相机是不现实的。
我想测试这些构造函数是否至少使用正确的参数被调用,证明解析工作正常。我已经考虑过分别抽象出解析和测试,但从技术上讲,仍然有构造函数调用未被测试。我也总是被告知,为了测试而添加功能通常是应该避免的,所以这是我正在考虑的解决方案的另一件事。找到一种解决方案也很有趣,该解决方案允许在模拟/具体实现之间轻松进行编译时切换,以在循环中测试硬件。
版本
请注意,我使用的是C++ 17,CMake和最新版本的gtest。
正在考虑的解决方案
使用宏将构造函数调用更改为模拟构造函数调用。
在camerafactory.cpp
的顶部,添加类似以下内容的内容:
// camerafactory.cpp
#ifndef PRODUCTION
#include "MockCamera.hpp"
#define CameraA MockCameraA
#define CameraB MockCameraB
#define CameraC MockCameraC
#endif
其中MockCameraN
在具有相同构造函数的单独 hpp/cpp 文件中定义,并且还实现了相同的相机接口,然后我可以使用它来测试正确的构造。
这将允许我测试构造函数是否正确调用,但确实感觉它违背了可以使用 Java 等语言完成的更干净的解决方案。
使用 GMock
我还没有深入研究它,但GMock似乎很有希望:http://google.github.io/googletest/gmock_cook_book.html
无论有没有gmock,我的建议是使用可以在测试代码中重写的虚拟方法。
首先,将工厂函数make_camera
转换为自己的类,其中包含要验证的内容的虚拟方法。 为简洁起见,我使用的是内联方法。在生产代码中,您可能希望将其分离到自己的 .h 文件中(用于类声明)和.cpp文件(用于实现)。
class CameraFactory
{
public:
virtual Camera* make_camera_a(int param) {
return new CameraA(param);
}
virtual Camera* make_camera_b(const string& param) {
return new CameraB(param);
}
virtual Camera* make_camera_c(double param1, param2) {
return new CameraC(param1, param2);
}
virtual Camera* make_camera(const Config& config) {
switch (config.type) {
case CameraTypes::A: {
return make_camera_a(config.intParam);
}
case CameraTypes::B: {
return make_camera_b(config.strParam);
}
case CameraTypes::C: {
return make_camera_c(config.param1, config.param2);
}
default: {
throw std::invalid_argument("Invalid configuration!");
}
}
}
};
在你的产品代码中,你通常会调用make_camera
,你可以只使用堆栈外的CameraFactory:
替换产品代码中的类似这样的行:
Camera* cam = make_camera(config); // old code
有了这个:
CameraFactory factory;
Camera* cam = factory.make_camera(config);
无需 gmock 即可进行测试:
测试它的旧方法是让单元测试覆盖类中的各个 make 函数,该类部分模拟了您想要验证正确调用的虚拟方法。
class MockCamera : public Camera
{
};
class MockCameraFactory : public CameraFactory
{
virtual Camera* make_camera_a(int param) {
_camera_a_created = true;
_intParm = param;
return new MockCamera();
}
virtual Camera* make_camera_b(const string& param) {
_camera_b_created = true;
_strParam = param;
return new MockCamera();
}
virtual Camera* make_camera_c(double param1, param2) {
_camera_c_created = true;
_param1 = param1;
_param2 = param2;
return new MockCamera();
}
bool _camera_a_created;
bool _camera_b_created;
bool _camera_c_created;
int _intParam;
string _strParam;
double _param1;
double _param2;
CameraFactoryTest() {
resetState();
}
void resetState() {
_camera_a_created = false;
_camera_b_created = false;
_camera_c_created = false;
_intParam = 0;
_strParam = "";
_param1 = 0;
_param2 = 0;
}
};
请注意,CameraFactoryTest 不会覆盖make_camera
,但它会继承自它。
然后,单元测试代码可以验证是否将正确的参数传递给构造函数。 我只是猜测您的ut框架有一些名为UNIT_TEST_ASSERT
的验证宏。我相信你有类似的东西。
void test_make_camera()
{
MockCameraFactory factory;
Config config;
// test camera A
config.type=CameraConfig::A;
config.intParam = 42;
factory.make_camera(config);
UNIT_TEST_ASSERT(factory._camera_a_created == true);
UNIT_TEST_ASSERT(factory._camera_b_created == false);
UNIT_TEST_ASSERT(factory._intParam == 42);
// test camera B
factory.resetState();
config = {};
config.type=CameraConfig::B;
config.strPara = "USB1";
factory.make_camera(config);
UNIT_TEST_ASSERT(factory._camera_b_created == true);
UNIT_TEST_ASSERT(factory._strParam == "USB1");
...
}
使用谷歌测试 (GMock)
使用 gmock 会更容易,并使您的模拟类在其他测试中更有用。 而不是显式编码 MockCameraFactory 类,而是使用 GMock 方式。
首先声明一个从实际类继承的模拟类。
class MockCameraFactory : public CameraFactory
{
public:
MOCK_METHOD1(make_camera_a, Camera*(int param));
MOCK_METHOD1(make_camera_b, Camera*(const string& param));
MOCK_METHOD2(make_camera_c, Camera*(const string& param));
};
然后,您的测试代码可以执行以下操作:
class test_make_camera()
{
MockCameraFactoryTest factory;
MockCamera cameramock;
EXPECT_CALL(factory, make_camera_a(42)).WillOnce(Return(&cameramock));
Config config = {};
config.type = CameraType::A;
config.intParam = 42;
factory.make_camera(config);
Mock::VerifyAndClearExpectations(&factory);
}
我正在浏览如何设置 Google 测试的细节,以便它通过您自己的单元测试框架断言。 但你明白了。
另外值得注意的是 - 我最初在 CameraFactory 类中声明make_camera
是虚拟的。它实际上不一定是虚拟的,因为您的测试只是验证是否正确调用了make_camera_x
方法。