循环中的单元测试 With/Without 硬件

Unit Testing With/Without hardward in the loop

背景

我正在从事一个与各种相机及其独特的 API 接口的项目。所有相机都被抽象到一个接口中,特定于相机的 API 调用包含在它们自己的实现中 类.

问题说明

我正在尝试测试一种解析配置形式的工厂方法,以及 returns 指向新创建的相机的指针。这是一个高级视图:

// 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 工作。这对于某些测试情况确实有意义,即在循环中演示硬件,但在我的具体情况下,同时连接所有类型的相机是不现实的。

我想测试 这些构造函数是否至少使用正确的参数调用,证明解析工作正常。我考虑过将解析和测试分别抽象出来,但从技术上讲,仍然存在未测试的构造函数调用。我也总是被告知,为了测试而添加功能通常是应该避免的事情,所以这是我在解决方案中考虑的另一件事。找到一种允许在 mocks/concrete 实现之间轻松编译时切换以测试循环中的硬件的解决方案也很有趣。

版本

请注意,我使用的是 C++ 17、CMake 和最新版本的 gtest。

正在考虑的解决方案

使用宏将构造函数调用更改为 Mock 构造函数调用。

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 转换为它自己的 class,并使用虚拟方法来验证您想要验证的内容。为简洁起见,我使用了内联方法。在生产代码中,您可能希望将它分成自己的 .h 文件用于 class 声明和 .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 进行测试:

旧的测试方法是让您的单元测试覆盖 class 中的各个 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");

    ...

}

使用 Google 测试 (GMock)

使用 gmock 更容易,并使您的模拟 class 在其他测试中更有用。您无需显式编码 MockCameraFactory class,而是使用 GMock 方式。

首先声明一个继承自实际 class 的模拟 class。

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 class 中声明 make_camera 是虚拟的。它实际上不必是虚拟的,因为您的测试只是验证 make_camera_x 方法是否被正确调用。