单元测试析构函数?

20

有没有好的方法来进行析构函数单元测试?比如说我有一个类像下面这个(虚构)的例子:

class X
{
private:
    int *x;

public:
    X()
    {
         x = new int;
    }

    ~X()
    {
         delete x;
    }

    int *getX() {return x;}
    const int *getX() const {return x;}
};

有没有好的方法进行单元测试以确保x被删除,而不会在hpp文件中弄乱#define TEST或破坏封装?主要问题是很难确定x是否真正被删除,特别是因为对象在析构函数调用时处于作用域之外。


你可以使用具体对象而不是指针,析构函数将根据语言规则自动清理(即通过RAII模式)。如果您不想立即构造对象,则可以使用智能指针/包装器,例如std::unique_ptr或std::optional。当然,除非您正在编写自己的智能指针/包装器并且想要进行测试! :) - trev
7个回答

10

依赖注入可能有其优点。在构造函数中,不是创建一个对象(在本例中为 int,但在实际情况中更可能是用户定义的类型),而是将对象作为参数传递给构造函数。如果对象稍后创建,则需要将工厂传递给 X 的构造函数。

然后,在单元测试时,您可以传递模拟对象(或创建模拟对象的模拟工厂),析构函数记录它已被调用的事实。如果未调用,则测试失败。

当然,您无法模拟(或替换)内置类型,因此在这种特定情况下并没有好处,但如果使用接口定义对象/工厂,则可以实现依赖注入。

在单元测试中检查内存泄漏通常可以在更高层次上完成,正如其他人所说。但这只检查是否调用了某个析构函数,不能证明是否调用了正确的析构函数。例如,如果成员 x 的类型的析构函数缺少 "virtual" 声明,则无法捕获该问题(如果 x 只是一个 int,则无关紧要)。


1
是的,而且几乎是在同一时间。我猜我赢了,因为你停下来写代码示例了 :-) - Steve Jessop

10

我觉得您的问题在于当前示例无法进行测试。由于您想知道是否删除了x,因此您确实需要能够用模拟替换x。对于整型来说,这可能有点过头了,但我猜在您的实际示例中您有其他类。为了使其可测试,X构造函数需要请求实现int接口的对象:

template<class T>
class X
{
  T *x;
  public:
  X(T* inx)
    : x(inx)
  {
  }

  // etc
};

现在,模拟x的值变得简单了,而且模拟可以处理正确销毁的检查。
请不要理会那些说你应该打破封装或采用可怕的hack来实现可测试代码的人。虽然经过测试的代码比未经测试的代码更好,但可测试的代码是最好的,它总是导致更清晰、更少hack和更低耦合的代码。

这是处理int类型的唯一方法,但类模板并不是类的简单替代品。在任何地方都使用模板通常会限制您对DLL的选项,减少头文件依赖等。当然,所有这些都取决于编译器:我听说有些编译器支持更好的模板链接。 - Steve Jessop
请注意,也许我只是因为说您无法模拟内置类型而变得有些暴躁,但正如您所指出的那样,这是错误的,因为您可以通过模板和实现整个int接口的类以此方式进行操作(不是微不足道的,但您只需要做一次。别忘了numeric_limits)。 - Steve Jessop
好吧,您不必使其成为模板化的。如果您从我的示例中删除了模板并将T改为int,则仍然可以进行测试 - 1800 INFORMATION

2
我倾向于采用“不择手段”的测试方法。如果需要进行测试,我愿意泄露抽象、破坏封装和进行黑客攻击……因为经过测试的代码比漂亮的代码更好。我通常会将打破封装的方法命名为 VaildateForTesting 或 OverrideForTesting,以明确表示这种破坏封装是仅用于测试的。
我不知道在 C++ 中有没有其他方式来实现这一点,除了让析构函数调用单例来注册它已被销毁。我已经想出了一种类似于 C# 的使用弱引用的方法(我不会违反封装或抽象)。我不够有创意来想出一个类比 C++ 的方法,但是你可能可以。如果有帮助,那太好了,如果没有,抱歉。

http://houseofbilz.com/archive/2008/11/11/writing-tests-to-catch-memory-leaks-in-.net.aspx


1
在这个例子中,定义并实现你自己的全局new和delete函数。
为了避免使用#ifdef,我将测试类设置为友元。你可以根据需要设置/保存/获取状态来验证调用的结果。

这个想法不具备可扩展性。如果你需要测试很多不同的类,你会实现不同版本的全局new和delete吗?如果你正在对已经替换了::new和::delete的代码进行单元测试,那该怎么办? - 1800 INFORMATION
我不知道你在想象什么草人。这个工具旨在回答类似于“x是否仍然被分配”的问题。其他答案利用了特定平台版本的内存检测工具,而不是自己控制。 - Tony Lee
你的设计很糟糕。为什么必须修改测试中所用到的类,才能让测试工具正常运作呢?如果你一开始就使你的类可测试(参考我的示例),你就不需要采取这样的方法了。 - 1800 INFORMATION
你还在想象我没有说过的话。 - Tony Lee
1
你说过:“我已经让测试类成为了朋友” - 这是修改被测试的类以使你的测试工具与之配合。你不应该这样做。 - 1800 INFORMATION

1

这对问题提出者可能不相关,但对于其他阅读此内容的人可能会有所帮助。我在一次工作面试中被问了一个类似的问题。

假设内存受限,您可以尝试以下方法:

  1. 分配内存,直到分配失败并显示内存不足消息(在运行任何相关测试之前),并保存在运行测试之前可用内存的大小。
  2. 运行测试(调用构造函数并在新实例上执行一些操作)。
  3. 运行析构函数。
  4. 再次运行分配部分(如步骤1所述) 如果您能够分配与运行测试之前成功分配的完全相同的内存,则说明析构函数正常工作。

当内存较小且受限时,此方法是有效的(相对而言),否则它似乎不太合理,至少是我个人的意见。


0

这并不是一个平台无关的建议,但在过去,我曾在单元测试期间调用CRT的堆检查函数,以验证在测试结束时(或整个测试集)分配的内存量是否与开始时相同。您也可以使用平台的工具来进行类似的操作,以检查句柄计数等。


0

一些编译器在调试模式下会使用已知的模式覆盖已删除的内存,以帮助检测对悬空指针的访问。我知道 Visual C++ 曾经使用 0xDD,但我已经有一段时间没有使用它了。

在您的测试用例中,您可以存储 x 的副本,让它超出范围,并确保 *x == 0xDDDDDDDD:

void testDestructor()
{
    int *my_x;
    {
        X object;
        my_x = object.getX();
    }
    CPPUNIT_ASSERT( *my_x == 0xDDDDDDDD );
}

我鄙视这个想法。如果启用了优化或编译器更改,你就无法对代码进行单元测试。 - 1800 INFORMATION
我同意你提到的原因不太好,而且你无法访问私有指针。一般来说,我认为你和onebyone的想法更好,但使用模拟对象并不总是那么简单。 - Matthew Crumley

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