【问题标题】:Unit testing destructors?单元测试析构函数?
【发布时间】:2010-09-23 15:22:28
【问题描述】:

有什么好的方法可以对析构函数进行单元测试吗?就像说我有一个像这样(人为)的类的例子:

class X
{
private:
    int *x;

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

    ~X()
    {
         delete x;
    }

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

有没有什么好的方法可以对它进行单元测试,以确保 x 被删除,而不会用 #ifdef TESTs 弄乱我的 hpp 文件或破坏封装?我看到的主要问题是很难判断 x 是否真的被删除了,特别是因为在调用析构函数时对象超出了范围。

【问题讨论】:

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

标签: c++ unit-testing destructor cppunit


【解决方案1】:

这与提出问题的人无关,但可能对阅读此问题的其他人有所帮助。我在一次求职面试中被问到了类似的问题。

假设内存有限,可以试试这个方法:

  1. 分配内存直到分配失败并显示内存不足消息(在对析构函数运行任何相关测试之前)并在运行测试之前保存可用内存的大小。
  2. 运行测试(调用构造函数并根据需要对新实例执行一些操作)。
  3. 运行析构函数。
  4. 再次运行分配部分(如第 1 步) 如果您可以分配与在运行测试之前设法分配的内存完全相同的内存,则析构函数可以正常工作。

当您的内存有限时,此方法(合理)有效,否则它似乎不合理,至少在我看来是这样。

【讨论】:

    【解决方案2】:

    我认为您的问题是您当前的示例不可测试。由于您想知道x 是否被删除,因此您确实需要能够用模拟替换x。对于 int 来说,这可能有点 OTT,但我想在您的真实示例中,您还有其他类。为了使其可测试,X 构造函数需要请求实现int 接口的对象:

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

    现在模拟x 的值变得很简单,并且模拟可以处理检查是否正确销毁。

    请不要注意那些说您应该打破封装或诉诸可怕的黑客攻击以产生可测试代码的人。虽然经过测试的代码确实比未经测试的代码更好,但可测试的代码是最好的,它总是会产生更清晰的代码,更少的黑客攻击和更低的耦合。

    【讨论】:

    • 这是对 int 执行此操作的唯一方法,但类模板并不是类的简单替代品。在任何地方使用模板通常会限制您对 DLL 的选择,减少对头文件的依赖等。当然,这一切都取决于编译器:有人告诉我,有些人支持比其他人更好的模板链接。
    • 请注意,也许我只是脾气暴躁,因为我说您不能模拟内置类型,正如您指出的那样,这是错误的,因为您可以通过模板以这种方式进行操作和一个实现整个 int 接口的类(不是微不足道的,但你只需要做一次。不要忘记 numeric_limits)。
    • 好吧,您不必将其模板化。如果您在我的示例中删除了模板并将 T 更改为 int ,它仍然是可测试的
    【解决方案3】:

    对于依赖注入可能有些话要说。与其在其构造函数中创建对象(在这种情况下为 int,但在非人为的情况下更可能是用户定义的类型),不如将对象作为参数传递给构造函数。如果对象是稍后创建的,则将工厂传递给 X 的构造函数。

    然后,当您进行单元测试时,您传入一个模拟对象(或创建模拟对象的模拟工厂),析构函数记录它已被调用的事实。如果不是,则测试失败。

    当然,您不能模拟(或以其他方式替换)内置类型,因此在这种特殊情况下它不好,但如果您使用接口定义对象/工厂,那么您可以。

    正如其他人所说,在单元测试中检查内存泄漏通常可以在更高级别完成。但这仅检查 a 析构函数是否被调用,并不能证明 right 析构函数被调用。所以它不会,例如在 x 成员类型的析构函数上捕获缺少的“虚拟”声明(同样,如果它只是一个 int,则不相关)。

    【讨论】:

    • 是的,而且几乎是在同一时间。我想我赢了是因为你停下来写代码示例 :-)
    【解决方案4】:

    在示例中,定义和检测您自己的全局 new 和 delete。

    为了避免#ifdefs,我交了测试班的朋友。您可以根据需要设置/保存/获取状态来验证调用结果。

    【讨论】:

    • 这个想法无法扩展。如果你需要测试很多不同的类,你会实现不同版本的全局 new 和 delete 吗?如果您的单元测试代码已经替换了 ::new 和 ::delete 怎么办?
    • 我不知道你想象中的稻草人。该仪器旨在回答“x 是否仍然分配”之类的问题?这里的其他答案利用了特定于平台的内存检测版本,而不是自己控制。
    • 你的设计很差。为什么你必须修改你的测试类以便你的测试工具可以使用它?如果您从一开始就使您的课程可测试(请参阅我的示例),则无需诉诸此类事情
    • 你还在想象我没有说的事情。
    • 你说“我已经交了测试类的朋友”——这是修改被测类以使你的测试工具与它一起工作。你不应该这样做。
    【解决方案5】:

    不是与平台无关的建议,但过去我在单元测试期间调用了 CRT 的堆检查函数,以验证在测试结束时没有分配更多内存(或者可能是整个测试集) 比开始。您也许还可以对平台的仪器执行类似的操作,以检查句柄计数等。

    【讨论】:

      【解决方案6】:

      我倾向于采用“以任何必要的方式”进行测试。如果它需要测试,我愿意泄漏抽象、破坏封装和破解……因为经过测试的代码比漂亮的代码更好。我经常将打破这种情况的方法命名为 VaildateForTesting 或 OverrideForTesting,以明确违反封装的目的仅用于测试。

      除了将析构函数调用到单例中以注册它已被销毁之外,我不知道在 C++ 中执行此操作的任何其他方法。我想出了一种在 C# 中使用弱引用做类似事情的方法(我不违反这种方法的封装或抽象)。我没有足够的创造力来与 C++ 进行类比,但你可能会。如果有帮助,那就太好了,如果没有,对不起。

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

      【讨论】:

        【解决方案7】:

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

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

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

        【讨论】:

        • 我鄙视这个想法。您无法在启用优化或编译器更改的情况下对代码进行单元测试
        • 我同意这不是很好,因为你提到的原因,你不能访问私有指针。一般来说,我认为您的想法(以及其他人的想法)更好,但使用模拟对象并不总是微不足道的。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-06-13
        • 2013-03-23
        • 2017-09-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多