我应该将成员函数声明为虚函数只是为了让一个类变得可测试吗?

8

我正在编写一个类,其简化版本如下:

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,也不想成为一个TDD信徒,但对于像extract_content_type这样的函数进行测试更容易一些。

1
我会说选项2是最不邪恶的。使用虚函数,您的方法可以在派生类中公开暴露。 - juanchopanza
同样地,如果需要,您可以使用template<>或类似的东西来包装您的类并在条件编译下声明特定于模板的友元,但这有点复杂。选择简单的方法。 - WhozCraig
6个回答

4
答案是不需要测试私有函数。理想情况下,你甚至不应该编写它们,而是通过重构来创建它们(尽管我承认这在实践中非常困难)。
当测试公共/受保护的函数时,应该隐式地测试您的私有函数。如果私有函数的功能没有得到充分断言,那么这意味着该函数执行的操作在类外部没有任何可见效果。
这不仅仅是TDD问题。由于私有函数是实现细节,我通常假设可以重构它们而不会破坏任何内容。如果某个函数已经进行了测试,而我决定重构其签名,则该测试将不再适用,这会让我非常困惑。

1
我不同意。特别是,您在第二段中提出的论点同样可以用来反驳单元测试的整个概念:为什么要测试特定的代码单元?测试整个系统应该隐含地测试每个单元。如果它没有测试每个单元的全部内容,那么显然该单元执行的操作对系统没有任何可见效果。我不太喜欢使用私有成员函数(通常我更喜欢使用非成员辅助函数),但我认为测试它们没有问题。 - James McNellis
1
好观点。我想反驳一下,应该测试单元,因为它们可能在编程时无法预见的上下文中使用(非成员助手也是如此)。然而,私有成员仅在包含它们的类的上下文中使用,因此不需要进行显式测试。不过,我承认我们不应该对这些准则过于死板(正如尊敬的巴博萨船长所说:“第三,代码更多的是‘指南’而不是实际规则。”)。 - Björn Pollex

3

1
我感觉这里的 #ifdef 不必要。如果有人将其类命名为 FooTest 以搞乱代码,那么完全可以定义 UNITTEST - Matthieu M.
但这基本上意味着测试私有函数。在测试中这不是一个很好的选择,对吧? - futlib
@futlib 我不确定我是否理解您的意思。我认为将不合格测试代码放在一个UnitTest类中是一种更符合c++的面向对象编程方式。 - chyx

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 

您可以在这里阅读更多信息。当处理单元测试中的非公共成员时,这是非常实用的工具。

0

我可以提供一个不同的选择,但我不确定你是否会喜欢。

你可以创建一个

#define TESTING_VIRTUAL

这将根据编译时选项扩展为virtual或无。因此,如果您正在编译测试,则可以将其设置为用virtual替换,如果是生产,则不会是虚拟方法。

如果宏扩展为privatepublic,则在测试模式下编译时可能会出现相同的情况。


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

我非常喜欢这个建议。然而,我没有告诉你除了提取其内容类型并将其打印出来之外,我不会对请求做任何事情,所以我想保持简单。 - futlib
好的。我认为在这种情况下,你的第二个选项是最好的。可能使用一些命名空间来指示它与服务器类的连接。 - Jason

0

我同意Björn的观点。一个类是否有私有函数是由该类自己决定的,与调用者无关。如果您删除了该私有方法,也就是说您决定直接在start函数中提取内容类型并不那么困难,会发生什么情况呢?好吧,您将破坏测试用例,即使该类按照应该的方式工作。私有就是私有!:)

我的建议是将您的extract_content_type放入一个用于内容处理的实用程序类中,并在测试用例中使用该类。这样就不需要存在服务器代码来测试该类。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接