【问题标题】:C++ - Is it possible to implement memory leak testing in a unit test?C++ - 是否可以在单元测试中实现内存泄漏测试?
【发布时间】:2011-02-28 04:24:45
【问题描述】:

我正在尝试为我的代码实施单元测试,但我很难做到。

理想情况下,我想测试一些类,不仅是为了获得良好的功能,还要为了正确的内存分配/释放。我想知道这个检查是否可以使用单元测试框架来完成。我正在使用Visual Assert 顺便说一句。如果可能的话,我希望看到一些示例代码!

【问题讨论】:

    标签: c++ visual-studio unit-testing memory-leaks


    【解决方案1】:

    您可以直接在开发工作室中使用调试功能来执行泄漏检查 - 只要您的单元测试使用调试 c 运行时运行。

    一个简单的例子如下所示:

    #include <crtdbg.h>
    struct CrtCheckMemory
    {
      _CrtMemState state1;
      _CrtMemState state2;
      _CrtMemState state3;
      CrtCheckMemory()
      {
        _CrtMemCheckpoint(&state1);
      }
      ~CrtCheckMemory()
      {
        _CrtMemCheckpoint(&state2);
        // using google test you can just do this.
        EXPECT_EQ(0,_CrtMemDifference( &state3, &state1, &state2));
        // else just do this to dump the leaked blocks to stdout.
        if( _CrtMemDifference( &state3, &state1, &state2) )
          _CrtMemDumpStatistics( &state3 );
      }
    };
    

    并在单元测试中使用它:

    UNIT_TEST(blah)
    {
      CrtCheckMemory check;
    
      // TODO: add the unit test here
    
    }
    

    一些单元测试框架会自行分配 - 例如,Google 会在单元测试失败时分配块,因此任何因任何其他原因失败的测试块也总是会出现误报“泄漏”。

    【讨论】:

    • 这是一个非常“干净”的解决方案。我试过了(在一个公认的简单案例中),它按预期工作。 +1
    • 我会同意的。这是一个非常干净的解决方案。仅调用 _CrtDumpMemoryLeaks 函数不适用于 google 测试,因为它错误地报告了框架中的一些泄漏,但此解决方案避免了上述问题。不过,您必须记住在每个测试用例的顶部创建该类的实例。
    • 我刚刚成功地将这个解决方案添加到一大组数百个测试中。您可以将指针作为类变量添加并在 TEST_METHOD_INITIALIZE 中分配它并在 TEST_METHOD_CLEANUP 中删除它。这样每个 TEST_CLASS 只有一次
    • @Chris Becke 如何在 Linux 上执行您建议的相同解决方案(我在以前的项目中使用的是在 Windows 下开发的)(我使用 Eclipse 和 CLion IDE 以防万一) ?
    • 我真的无能为力。列出的解决方案利用了 microsoft c/c++ 运行时的内置泄漏检测。 CLion 支持 CLang 和 GCC 作为其编译器,因此您需要在它们的运行时中找到等效的支持(如果存在),或者第三方堆替换。
    【解决方案2】:

    您可以使用 Google 的 tcmalloc 分配库,它提供了一个 heapchecker

    (请注意,堆检查可能会显着增加程序性能的开销,因此您可能只想在调试构建或单元测试时启用它。)

    你要求提供示例代码,所以here it is

    【讨论】:

    【解决方案3】:
    1. 经过我的一些调查并基于 Chris Becke 的非常好的解决方案(适用于 Windows),我为 Linux 操作系统制作了一个非常相似的解决方案。

    2)我的内存泄漏检测目标:

    非常清楚 - 同时检测泄漏:

    2.1) 理想情况下以精确的方式 - 准确指出分配了多少字节但尚未释放。

    2.2) 尽力而为 - 如果不准确,请以“误报”的方式指出(即使不一定是泄漏,也要告诉我们泄漏,同时不要错过任何泄漏检测)。在这里对自己严苛一些比较好。

    2.3) 由于我在 GTest 框架中编写单元测试 - 将每个 GTest 单元测试作为“原子实体”进行测试。

    2.4) 还要考虑使用 malloc/free 的“C 风格”分配(释放)。

    2.5) 理想情况下 - 考虑 C++“就地分配”。

    2.6) 易于使用并集成到现有代码中(用于单元测试的基于 GTest 的类)。

    2.7) 能够为每个测试和/或整个测试类“配置”主要检查设置(启用/禁用内存检查等)。

    1. 解决方案架构:

    我的解决方案使用 GTest 框架的继承功能,因此它为我们将来添加的每个单元测试类定义了一个“基”类。基本上,基类的主要功能可以分为以下几类:

    3.1) 运行“第一个”GTest 样式测试,以了解在测试失败的情况下分配在堆上的“额外内存”数量。正如 Chris Becke 在其答案的最后一句中提到的那样。

    3.2) 易于集成 - 只需从这个基类继承并编写您的单元测试“TEST_F 风格”函数。

    3.3.1) 对于每个测试,我们可以决定是否另外声明执行内存泄漏检查。这是通过 SetIgnoreMemoryLeakCheckForThisTest() 方法完成的。 注意:无需再次“重置”它 - 由于 GTest 单元测试的工作方式(它们在每个函数调用之前调用 Ctor),它将在下一次测试中自动发生。

    3.3.2) 此外,如果由于某种原因您事先知道您的测试将“错过”一些内存释放并且您知道数量 - 您可以利用这两个函数来考虑这一事实一次执行内存检查(顺便说一下,通过“简单地”从测试结束时使用的内存量中减去测试开始时使用的内存量来执行)。

    下面是头基类:

    // memoryLeakDetector.h:
    #include "gtest/gtest.h"
    extern int g_numOfExtraBytesAllocatedByGtestUponTestFailure;
    
    // The fixture for testing class Foo.
    class MemoryLeakDetectorBase : public ::testing::Test 
    {
    // methods:
    // -------
    public:
        void SetIgnoreMemoryLeakCheckForThisTest() { m_ignoreMemoryLeakCheckForThisTest= true; } 
        void SetIsFirstCheckRun() { m_isFirstTestRun = true; }
    
    protected:
    
        // You can do set-up work for each test here.
        MemoryLeakDetectorBase();
    
        // You can do clean-up work that doesn't throw exceptions here.
        virtual ~MemoryLeakDetectorBase();
    
        // If the constructor and destructor are not enough for setting up
        // and cleaning up each test, you can define the following methods:
    
        // Code here will be called immediately after the constructor (right
        // before each test).
        virtual void SetUp();
    
        // Code here will be called immediately after each test (right
        // before the destructor).
        virtual void TearDown();
    
    private:
        void getSmartDiff(int naiveDiff);
        // Add the extra memory check logic according to our 
        // settings for each test (this method is invoked right
        // after the Dtor).
        virtual void PerformMemoryCheckLogic();
    
    // members:
    // -------
    private:
        bool m_ignoreMemoryLeakCheckForThisTest;
        bool m_isFirstTestRun;
        bool m_getSmartDiff;
        size_t m_numOfBytesNotToConsiderAsMemoryLeakForThisTest;
        int m_firstCheck;
        int m_secondCheck;
    };
    

    这里是这个基类的来源:

    // memoryLeakDetectorBase.cpp
    #include <iostream>
    #include <malloc.h>
    
    #include "memoryLeakDetectorBase.h"
    
    int g_numOfExtraBytesAllocatedByGtestUponTestFailure = 0;
    
    static int display_mallinfo_and_return_uordblks()
    {
        struct mallinfo mi;
    
        mi = mallinfo();
        std::cout << "========================================" << std::endl;
        std::cout << "========================================" << std::endl;
        std::cout << "Total non-mmapped bytes (arena):" << mi.arena << std::endl;
        std::cout << "# of free chunks (ordblks):" << mi.ordblks << std::endl;
        std::cout << "# of free fastbin blocks (smblks):" << mi.smblks << std::endl;
        std::cout << "# of mapped regions (hblks):" << mi.hblks << std::endl;
        std::cout << "Bytes in mapped regions (hblkhd):"<< mi.hblkhd << std::endl;
        std::cout << "Max. total allocated space (usmblks):"<< mi.usmblks << std::endl;
        std::cout << "Free bytes held in fastbins (fsmblks):"<< mi.fsmblks << std::endl;
        std::cout << "Total allocated space (uordblks):"<< mi.uordblks << std::endl;
        std::cout << "Total free space (fordblks):"<< mi.fordblks << std::endl;
        std::cout << "Topmost releasable block (keepcost):" << mi.keepcost << std::endl;
        std::cout << "========================================" << std::endl;
        std::cout << "========================================" << std::endl;
        std::cout << std::endl;
        std::cout << std::endl;
    
        return mi.uordblks;
    }
    
    MemoryLeakDetectorBase::MemoryLeakDetectorBase() 
        : m_ignoreMemoryLeakCheckForThisTest(false)
        , m_isFirstTestRun(false)
        , m_getSmartDiff(false)
        , m_numOfBytesNotToConsiderAsMemoryLeakForThisTest(0)
    {
        std::cout << "MemoryLeakDetectorBase::MemoryLeakDetectorBase" << std::endl;
        m_firstCheck = display_mallinfo_and_return_uordblks();
    }
    
    MemoryLeakDetectorBase::~MemoryLeakDetectorBase() 
    {
        std::cout << "MemoryLeakDetectorBase::~MemoryLeakDetectorBase" << std::endl;
        m_secondCheck = display_mallinfo_and_return_uordblks();
        PerformMemoryCheckLogic();
    }
    
    void MemoryLeakDetectorBase::PerformMemoryCheckLogic()
    {
        if (m_isFirstTestRun) {
            std::cout << "MemoryLeakDetectorBase::PerformMemoryCheckLogic - after the first test" << std::endl;
            int diff = m_secondCheck - m_firstCheck;
            if ( diff > 0) {
                std::cout << "MemoryLeakDetectorBase::PerformMemoryCheckLogic - setting g_numOfExtraBytesAllocatedByGtestUponTestFailure to:" << diff << std::endl;
                g_numOfExtraBytesAllocatedByGtestUponTestFailure = diff;
            }
            return;
        }
    
        if (m_ignoreMemoryLeakCheckForThisTest) {
            return;
        }
        std::cout << "MemoryLeakDetectorBase::PerformMemoryCheckLogic" << std::endl;
    
        int naiveDiff = m_secondCheck - m_firstCheck;
    
        // in case you wish for "more accurate" difference calculation call this method
        if (m_getSmartDiff) {
            getSmartDiff(naiveDiff);
        }
    
        EXPECT_EQ(m_firstCheck,m_secondCheck);
        std::cout << "MemoryLeakDetectorBase::PerformMemoryCheckLogic - the difference is:" << naiveDiff << std::endl;
    }
    
    void MemoryLeakDetectorBase::getSmartDiff(int naiveDiff)
    {
        // according to some invastigations and assumemptions, it seems like once there is at least one 
        // allocation which is not handled - GTest allocates 32 bytes on the heap, so in case the difference
        // prior for any further substrcutions is less than 32 - we will assume that the test does not need to 
        // go over memory leak check...
        std::cout << "MemoryLeakDetectorBase::getMoreAccurateAmountOfBytesToSubstructFromSecondMemoryCheck - start" << std::endl; 
        if (naiveDiff <= 32) {
            std::cout << "MemoryLeakDetectorBase::getSmartDiff - the naive diff <= 32 - ignoring..." << std::endl;
            return;
        }
    
        size_t numOfBytesToReduceFromTheSecondMemoryCheck = m_numOfBytesNotToConsiderAsMemoryLeakForThisTest + g_numOfExtraBytesAllocatedByGtestUponTestFailure;
        m_secondCheck -= numOfBytesToReduceFromTheSecondMemoryCheck;
        std::cout << "MemoryLeakDetectorBase::getSmartDiff - substructing " << numOfBytesToReduceFromTheSecondMemoryCheck << std::endl;
    }
    
    void MemoryLeakDetectorBase::SetUp() 
    {
        std::cout << "MemoryLeakDetectorBase::SetUp" << std::endl;
    }
    
    void MemoryLeakDetectorBase::TearDown() 
    {
        std::cout << "MemoryLeakDetectorBase::TearDown" << std::endl;
    }
    
    // The actual test of this module:
    
    
    TEST_F(MemoryLeakDetectorBase, getNumOfExtraBytesGTestAllocatesUponTestFailureTest) 
    {
        std::cout << "MemoryLeakDetectorPocTest::getNumOfExtraBytesGTestAllocatesUponTestFailureTest - START" << std::endl;
    
        // Allocate some bytes on the heap and DO NOT delete them so we can find out the amount 
        // of extra bytes GTest framework allocates upon a failure of a test.
        // This way, upon our legit test failure, we will be able to determine of many bytes were NOT
        // deleted EXACTLY by our test.
    
        std::cout << "MemoryLeakDetectorPocTest::getNumOfExtraBytesGTestAllocatesUponTestFailureTest - size of char:" << sizeof(char) << std::endl;
        char* pChar = new char('g');
        SetIsFirstCheckRun();
        std::cout << "MemoryLeakDetectorPocTest::getNumOfExtraBytesGTestAllocatesUponTestFailureTest - END" << std::endl;
    }
    

    最后,一个示例“基于 GTest”的单元测试类使用此基类并说明用法和几个不同的 POC(概念证明),以用于各种不同的分配和验证,如果我们能够(或不能)检测到错过的取消分配。

    // memoryLeakDetectorPocTest.cpp
    #include "memoryLeakDetectorPocTest.h"
    #include <cstdlib>  // for malloc
    
    class MyObject 
    {
    
    public:
        MyObject(int a, int b) : m_a(a), m_b(b) { std::cout << "MyObject::MyObject" << std::endl; }
        ~MyObject() { std::cout << "MyObject::~MyObject" << std::endl; }  
    private:
        int m_a;
        int m_b;
    };
    
    MemoryLeakDetectorPocTest::MemoryLeakDetectorPocTest() 
    {
        std::cout << "MemoryLeakDetectorPocTest::MemoryLeakDetectorPocTest" << std::endl;
    }
    
    MemoryLeakDetectorPocTest::~MemoryLeakDetectorPocTest() 
    {
        std::cout << "MemoryLeakDetectorPocTest::~MemoryLeakDetectorPocTest" << std::endl;
    }
    
    void MemoryLeakDetectorPocTest::SetUp() 
    {
        std::cout << "MemoryLeakDetectorPocTest::SetUp" << std::endl;
    }
    
    void MemoryLeakDetectorPocTest::TearDown() 
    {
        std::cout << "MemoryLeakDetectorPocTest::TearDown" << std::endl;
    }
    
    TEST_F(MemoryLeakDetectorPocTest, verifyNewAllocationForNativeType) 
    {
    
        std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForNativeType - START" << std::endl;
    
        // allocate some bytes on the heap and intentially DONT release them...
        const size_t numOfCharsOnHeap = 23;
        std::cout << "size of char is:" << sizeof(char)  << " bytes" << std::endl;
        std::cout << "allocating " << sizeof(char) * numOfCharsOnHeap << " bytes on the heap using new []" << std::endl;
        char* arr = new char[numOfCharsOnHeap];
    
        // DO NOT delete it on purpose...
        //delete [] arr;
        std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForNativeType - END" << std::endl;
    }
    
    TEST_F(MemoryLeakDetectorPocTest, verifyNewAllocationForUserDefinedType) 
    {
        std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForUserDefinedType - START" << std::endl;
    
        std::cout << "size of MyObject is:" << sizeof(MyObject)  << " bytes" << std::endl;
        std::cout << "allocating MyObject on the heap using new" << std::endl;
        MyObject* myObj1 = new MyObject(12, 17);
    
        delete myObj1;
        std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForUserDefinedType - END" << std::endl;
    }
    
    TEST_F(MemoryLeakDetectorPocTest, verifyMallocAllocationForNativeType) 
    {
        std::cout << "MemoryLeakDetectorPocTest::verifyMallocAllocationForNativeType - START" << std::endl;
        size_t numOfDoublesOnTheHeap = 3;
        std::cout << "MemoryLeakDetectorPocTest::verifyMallocAllocationForNativeType - sizeof double is " << sizeof(double) << std::endl; 
        std::cout << "MemoryLeakDetectorPocTest::verifyMallocAllocationForNativeType - allocaitng " << sizeof(double) * numOfDoublesOnTheHeap << " bytes on the heap" << std::endl;
        double* arr = static_cast<double*>(malloc(sizeof(double) * numOfDoublesOnTheHeap));
    
        // NOT free-ing them on purpose !!
        // free(arr);
        std::cout << "MemoryLeakDetectorPocTest::verifyMallocAllocationForNativeType - END" << std::endl;
    }
    
    TEST_F(MemoryLeakDetectorPocTest, verifyNewAllocationForNativeSTLVectorType) 
    {
        std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForNativeSTLVectorType - START" << std::endl;
        std::vector<int> vecInt;
        vecInt.push_back(12);
        vecInt.push_back(15);
        vecInt.push_back(17);
    
        std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForNativeSTLVectorType - END" << std::endl;
    }
    
    TEST_F(MemoryLeakDetectorPocTest, verifyNewAllocationForUserDefinedSTLVectorType) 
    {
        std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForUserDefinedSTLVectorType - START" << std::endl;
        std::vector<MyObject*> vecMyObj;
        vecMyObj.push_back(new MyObject(7,8));
        vecMyObj.push_back(new MyObject(9,10));
    
        size_t vecSize = vecMyObj.size();
        for (int i = 0; i < vecSize; ++i) {
            delete vecMyObj[i];
        }
    
        std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForUserDefinedSTLVectorType - END" << std::endl;
    }
    
    TEST_F(MemoryLeakDetectorPocTest, verifyInPlaceAllocationAndDeAllocationForUserDefinedType) 
    {
         std::cout << "MemoryLeakDetectorPocTest::verifyInPlaceAllocationAndDeAllocationForUserDefinedType - START" << std::endl;
        void* p1 = malloc(sizeof(MyObject));
        MyObject *p2 = new (p1) MyObject(12,13);
    
        p2->~MyObject();
        std::cout << "MemoryLeakDetectorPocTest::verifyInPlaceAllocationAndDeAllocationForUserDefinedType - END" << std::endl;
    }
    
    TEST_F(MemoryLeakDetectorPocTest, verifyInPlaceAllocationForUserDefinedType) 
    {
        std::cout << "MemoryLeakDetectorPocTest::verifyInPlaceAllocationForUserDefinedType - START" << std::endl;
        void* p1 = malloc(sizeof(MyObject));
        MyObject *p2 = new (p1) MyObject(12,13);
    
        // Dont delete the object on purpose !!
        //p2->~MyObject();
        std::cout << "MemoryLeakDetectorPocTest::verifyInPlaceAllocationForUserDefinedType - END" << std::endl;
    }
    

    这个类的头文件:

    // memoryLeakDetectorPocTest.h
    #include "gtest/gtest.h"
    #include "memoryLeakDetectorBase.h"
    
    // The fixture for testing class Foo.
    class MemoryLeakDetectorPocTest : public MemoryLeakDetectorBase
    {
    protected:
    
        // You can do set-up work for each test here.
        MemoryLeakDetectorPocTest();
    
        // You can do clean-up work that doesn't throw exceptions here.
        virtual ~MemoryLeakDetectorPocTest();
    
        // Code here will be called immediately after the constructor (right
        // before each test).
        virtual void SetUp();
    
        // Code here will be called immediately after each test (right
        // before the destructor).
        virtual void TearDown();
    };
    

    【讨论】:

    • 堆栈溢出没问题 - 甚至鼓励提出问题然后自己回答,在这种情况下,我会推荐它,因为这个答案涵盖了与原始问题所解决的不同场景(及其标签)
    【解决方案4】:

    您可以通过在分配中添加内存跟踪信息来提供自己的newdeletemallocfree 函数实现,从而检测测试中的内存泄漏。

    【讨论】:

    • 好吧,您将不得不提供跟踪分配的分配器的替代实现。这并不意味着你必须自己写......
    • 没错。在线查找分配器库。 Nedalloc 是我认识的一个(食人魔使用它)。如果您不想使用类似的东西,那么您可以改用外部工具......但这在您的单元测试中将不可用。在 Visual Studio 中,有一个基本的内存泄漏检查器,但最好检测到有一个,而不是始终提供有关内存泄漏的信息...
    猜你喜欢
    • 1970-01-01
    • 2016-01-05
    • 2018-02-23
    • 1970-01-01
    • 2011-09-28
    • 1970-01-01
    • 2013-03-10
    • 1970-01-01
    相关资源
    最近更新 更多