【问题标题】:Should I make a member function virtual just to make a class testable?我应该让一个成员函数 virtual 只是为了让一个类可测试吗?
【发布时间】:2012-09-18 20:30:45
【问题描述】:

我正在编写一个简化版如下所示的课程:

class Http_server {
public:
    void start(int port)
    {
        start_server();
        std::string content_type = extract_content_type(get_request());
    }

private:
    void start_server()
    {
        ...
    }

    std::string get_request()
    {
        ...
    }

    std::string extract_content_type(const std::string& request) const
    {
        ...
    }
};

现在我想为extract_content_type 写一个测试用例。问题是:它是私人的,所以我不能从外面调用它。我可以测试的唯一函数是start,但实际上它会启动服务器 (start_server) 并等待请求 (get_request)。

我怎么看,我有三个选择:

  1. 公开extract_content_type
  2. extract_content_type 提取到实用程序类或命名空间中
  3. start_serverget_request 设为虚拟并创建一个模拟对象来覆盖它们

我不想公开任何内容或移动到仅在单个类中使用过一次的实用程序命名空间,因此最不邪恶的是选项 3。

我在 V8 代码库中至少看到过一个这样的例子: http://code.google.com/p/v8/source/browse/trunk/test/cctest/test-date.cc

不过,我不确定这是否是个好主意。 virtual 不是 C++ 中的默认值,原因有两个:

  1. 这会导致性能/内存开销(但在我的情况下可能无关紧要)
  2. 并非每个类都应该用作基类,使其明确也是设计决策

你会怎么做?与无用的虚拟生活在一起?还是根本不测试功能?我不喜欢 TDD,也不想这样做,但开发像 extract_content_type 这样的函数来测试会更容易。

【问题讨论】:

  • 我会说选项 2 是最不邪恶的。使用 virtual,您的方法可以在派生类中公开公开。
  • 同样。与#2。如果需要,您可以让您的测试用例使用模板 或类似的东西来包裹您的类,并在条件编译下将特定模板声明为朋友,但这有点复杂。选择简单。

标签: c++ unit-testing mocking virtual


【解决方案1】:

我可以向您推荐允许您使用私有方法的 API。它被称为 Typemock Isolator++。例如,我创建了一个测试,更改您的 extract_content_type 方法行为,调用它(尽管它是私有的),然后断言:

TEST_METHOD(TestExtractContentType)
    {   
        Http_server* server = new Http_server();

        std::string res ("result");
        PRIVATE_WHEN_CALLED(server, extract_content_type, NULL).Return(&res);

        std::string result;
        ISOLATOR_INVOKE_MEMBER(result, server, extract_content_type, NULL);

        PRIVATE_ASSERT_WAS_CALLED(server, extract_content_type);
        Assert::AreEqual(string("result"), result);
    }

无需更改代码。我刚刚为编译器添加了 ISOLATOR_TESTABLE 标记以确保。

ISOLATOR_TESTABLE std::string extract_content_type(const std::string& request) const 

您可以阅读更多here。在单元测试中处理非公共成员时非常方便。

【讨论】:

    【解决方案2】:

    我同意 Björn 在这一点上的看法。一个类有或没有什么私有功能取决于这个类,调用者不关心。如果您删除该私有方法会发生什么,也就是说您认为提取内容类型并不难,所以您直接在您的start 函数中执行它?好吧,您会破坏测试用例,尽管该类正在按应有的方式工作。私人就是私人! :)

    我的建议是您将extract_content_type 放在一个实用程序类中以进行内容处理,并在您的测试演员中使用该类。然后不需要提供服务器代码来测试这个类。

    【讨论】:

      【解决方案3】:

      我可以建议一个不同的选择,但我不确定你是否愿意。

      你可以创建一个

      #define TESTING_VIRTUAL
      

      这将扩展为 virtual 或任何内容,具体取决于编译时选项。所以如果你正在编译一个测试,你可以将它设置为替换为virtual,如果它是用于生产的,它就不是一个虚拟方法。

      如果宏扩展为privatepublic,也可能发生同样的情况,具体取决于您是否在测试模式下编译。

      【讨论】:

        【解决方案4】:

        答案是您不测试私有函数。理想情况下,您甚至不编写它们,而是通过重构来创建它们(尽管我承认这在实践中非常困难)。

        在测试公共/受保护函数时,应隐式测试您的私有函数。如果私有函数的功能没有以这种方式完全断言,那么这意味着该函数所做的事情在类之外没有可见的影响

        这不仅仅是一个 TDD 问题。由于私有函数是一个实现细节,我通常假设我可以在不破坏任何东西的情况下重构它们。如果有一个函数的测试,而我决定重构它的签名,那将不再适用,让我非常困惑。

        【讨论】:

        • 我不同意。具体来说,您在第二段中提出的论点可以很容易地用来完全反对单元测试:为什么要测试特定的代码单元?测试整个系统,这应该隐含地测试每个单元。如果它不测试每个单元的整体,那么显然该单元所做的事情在系统中没有可见的影响。我不太喜欢私有成员函数(我通常更喜欢非成员助手),但我认为测试它们没有任何问题。
        • 好点。我会反驳说应该测试单元,因为它们可能会在编程时无法预见的上下文中使用(非成员助手也是如此)。然而,私有成员只在包含它们的类的上下文中使用,因此不需要显式测试。我确实承认,人们不应该对这样的准则过于严格(正如受人尊敬的 Cpt. Barbossa 所说:第三,代码更像是你所谓的“准则”而不是实际规则。 )。
        【解决方案5】:

        我想你可能还有其他选择:

        让单元测试类成为你要测试的类的朋友

        class Foo {
          public:
        #ifdef UNITTEST
            friend class FooTest;
        #endif
            ...
        
          protected:
            ...
        
          private:
            ...
        };
        

        这是参考:http://praveen.kumar.in/2008/01/02/how-to-unit-test-c-private-and-protected-member-functions/

        【讨论】:

        • 我觉得这里不需要ifdef。如果将其类命名为FooTest 只是为了弄乱代码,那么无论如何都可以完美地定义UNITTEST
        • 但这本质上意味着测试私有函数。这不是测试中的禁忌吗?
        • @futlib 我不确定我是否理解你的意思。我认为将 no-go 测试代码包装在 UnitTest 类中是一种更适合 c++ 的 OOP 方式。
        【解决方案6】:

        如果 extract_content_type 不需要包含在您的 Http_server 类中的任何信息,则它不必属于该类。真的,看起来您需要一个请求本身的类,它可以返回自己的内容类型。然后可以测试该请求类。

        【讨论】:

        • 我真的很喜欢这个建议。但是,我没有告诉您,除了提取其内容类型并将其打印出来之外,我不会对请求执行任何操作,所以我想保持简单。
        • 对。我认为在这种情况下,您的第二个选择是最好的。可能使用一些命名空间来指示它与服务器类的连接。
        猜你喜欢
        • 1970-01-01
        • 2017-02-17
        • 1970-01-01
        • 2011-07-24
        • 1970-01-01
        • 1970-01-01
        • 2019-11-05
        • 2015-04-17
        • 1970-01-01
        相关资源
        最近更新 更多