1

我正在尝试帮助围绕 Qt C++ 应用程序中的控制器设计一些单元测试。

坦率地说,我有两个很大的缺点。第一,我的测试背景主要基于 .NET 项目,所以我对 c++ 世界中最佳实践的了解最多。第二,我正在查看的应用程序的设计者没有考虑到单元测试来构建代码。

一个具体点,我正在查看一个包含 boost/filesystem/operations.hpp 的控制器类。控制器构造函数继续检查目录是否存在并使用来自 boost 文件系统代码的函数创建目录。

有没有办法重载或模拟这种行为?我习惯于在 .NET 中设置 IoC 容器或至少依赖注入的构造函数,然后能够在单元测试代码中传递模拟对象。不过,我不确定模板化的头文件如何与该概念一起使用,或者它是否是 c++ 中的典型实践。

目前,我无法灵活地建议代码更改,因为本周即将发布版本。但在那之后,如果有一些简单的代码更改可以提高可测试性,那绝对是一个选择。理想情况下,有一种方法可以在单元测试框架中按原样重载文件系统功能。

4

3 回答 3

1

我们最终创建了一个通用文件系统包装器,它调用 Boost 文件系统并将其作为参数接受给我们的类构造函数,以便我们可以在单元测试时发送模拟版本。

我理解不要嘲笑这一点的想法,但我认为快速单元测试对于我们的 CI 环境在签入时运行以及实际命中文件系统的测试是有价值的。

于 2013-05-08T17:07:00.827 回答
1

如果您考虑一下,您需要将 IFilesystem 的实例注入到您的类中的唯一原因是在测试中模拟它。无论如何,您的非测试代码库的任何部分都不会使用任何东西,但真正的文件系统,因此可以安全地假设您可以将类型注入到您的类中,并在您的代码库中自由使用它们而不会发生类型冲突。

所以,假设你有兴趣班

struct BuildTree
{
    BuildTree(std::string_view dirname) { /*...*/ }

    bool has_changed()
    {
        // iterates through files in directory
        // recurs into directories
        // checks the modification date against lastModified_
        // uses boost::filesystem::last_write_time(), etc.
    }

private:
    boost::filesystem::file_time_type lastModified_;
};

现在,您可以将类型注入到类中。这种类型将是一个带有一堆静态方法的类。会有一个RealFilesystem类型会重定向到 boost::filesystem 方法,并且会有一个SpyFilesystem.

template <class Filesystem>
struct BuildTree
{
    // ...
    bool has_changed()
    {
        // uses Filesystem::last_write_time(), etc.
    }

private:
    typename Filesystem::file_time_type lastModified_;
};

SpyFilesystem将类似于 PIMPL 习惯用法,因为静态方法会将调用重定向到实际实现。

struct SpyFilesystemImpl;

struct SpyFilesystem
{
    using file_time_type = typename SpyFilesystemImpl::file_time_type;

    static file_time_type last_time_write(std::string_view filename)
    {
        return instance.last_time_write(filename);
    }
    
    // ... more methods

    static SpyFilesystemImpl instance;
};

SpyFilesystemImpl SpyFilesystem::instance{};

// No warranty of completeness provided
struct SpyFilesystemImpl
{
    using file_time_type = std::chrono::system_clock::time_point;

    void create_directory(std::string_view path) { /*...*/ }
    void touch(std::string_view filename)
    {
        ++lastModified_[filename];
    }
    
    file_time_type last_time_write(std::string_view filename)
    {
        return std::chrono::system_clock::time_point{lastModified_[filename]};
    }

private:
    std::unordered_map<std::string, std::chrono::seconds> lastModified_;
};

最后,在您的每个测试中,您将准备一个 实例SpyFilesystemImpl,将其分配给SpyFilesystem::instance,然后使用 实例化您的类SpyFilesystem。像这样

// ... our SpyFilesystem and friends ...

// Google Test framework
TEST(BuildTree, PickUpChanges)
{
    SpyFilesystemImpl fs{};
    fs.create_directory("foo");
    fs.touch("foo/bar.txt");

    SpyFilesystem::instance = fs;

    BuildTree<SpyFilesystem> tree("foo");
    EXPECT_FALSE(tree.has_changed());

    SpyFilesystem::instance.touch("foo/bar.txt");
    EXPECT_TRUE(tree.has_changed());
}

这种方法的优点是生成的二进制文件中没有运行时开销(前提是启用了优化)。但是,它需要更多样板代码,这可能是个问题。

于 2021-09-24T22:39:12.997 回答
0

boost::filesystem在我看来,将其视为标准库的扩展似乎是合理的。所以你模拟它(或不模拟它)的方式与模拟类似std::istream. (通常,当然,您不会模拟它,而是您的测试框架提供了必要的环境:您需要读取的文件std::istream、目录等 boost::filesystem。)

于 2013-04-23T17:26:03.930 回答