单元测试c++.如何测试私有成员

Unit testing c++. How to test private members?

本文关键字:成员 测试 c++ 何测试 单元测试      更新时间:2023-10-16

我想为我的c++应用程序做单元测试。

测试类的私有成员的正确形式是什么?创建一个友类来测试私有成员,使用派生类或其他技巧?

测试api使用哪种技术?

通常,只测试问题注释中讨论的公共接口。

然而,有时测试私有或受保护的方法是有帮助的。例如,实现可能对用户隐藏一些重要的复杂性,可以通过访问非公共成员更精确地进行测试。通常情况下,最好找出一种方法来消除这种复杂性,或者找出如何公开相关部分,但并非总是如此。

允许单元测试访问非public成员的一种方法是通过friend构造。

回答这个问题涉及到许多其他话题。除了在CleanCode, TDD和其他方面的任何宗教信仰:

有几种访问私有成员的方法。在任何情况下,你都必须否决测试过的代码!这在解析c++的两个层次(预处理器和语言本身)上都是可能的:

Define all to public

通过使用预处理器,你可以打破封装。

#define private public
#define protected public
#define class struct

缺点是,交付代码的类与测试中的不一样!c++标准第9.2.13章说:

非静态数据成员的分配顺序不同访问控制未指定。

这意味着编译器有权为测试重新排序成员变量和虚函数。如果没有缓冲区溢出发生,您可能会觉得这不会损害您的类,但这意味着您不会测试与交付相同的代码。这意味着,如果你访问一个对象的成员,这个对象是由代码初始化的,private没有定义为public,那么你的成员的偏移量可能会不同!

该方法需要更改被测试类,以使其与测试类或测试函数成为朋友。一些测试框架,如gtest (FRIEND_TEST(..);)有特殊的功能来支持这种访问私有事物的方式。

class X
{
private:
    friend class Test_X;
};

它仅为测试打开类,而不打开世界,但是您必须修改交付的代码。在我看来,这是一件坏事,因为测试不应该改变被测试的代码。作为进一步的缺点,它使交付代码的其他类有可能通过将自己命名为测试类来入侵您的类(这也会损害c++标准的ODR规则)。

为测试声明受保护的私有对象和从类派生的私有对象

不是一个很优雅的方式,很有侵入性,但也可以使用:

class X
{
protected:
    int myPrivate;
};
class Test_X: public X
{
    // Now you can access the myPrivate member.
};

使用宏的其他方法

有效,但与第一种方法在标准一致性方面存在相同的缺点。例如:

class X
{
#ifndef UNITTEST
private:
#endif
};

我认为最后两种方法不能替代前两种方法,因为它们没有前两种方法的优势,但对测试代码的干扰更大。第一种方法是非常危险的,所以你可以使用友好的方法。


关于never-test-private-things-discussion的一些话。单元测试的一个好处是,您可以很早就到达必须改进代码设计的阶段。这有时也是单元测试的缺点之一。它有时会使面向对象变得更加复杂。特别是当你按照与现实世界中的对象相同的方式来设计类时。

那么,您有时不得不将代码更改为一些丑陋的东西,因为单元测试方法迫使您这样做。处理用于控制物理过程的复杂框架就是一个例子。在这种情况下,您需要将代码映射到物理过程上,因为过程的某些部分通常已经非常复杂。这些进程的依赖列表有时会很长。这是一个可能的时刻,测试私有成员变得越来越好。你必须权衡每种方法的优缺点。

类有时变得复杂!然后你必须决定把它们分开还是接受它们。有时第二个决定更有意义。最后的问题总是你想要实现的目标(例如,完美的设计,快速的整合时间,低开发成本…)。


我认为

我访问私有成员的决策过程是这样的:

  1. 你需要自己测试私有成员吗?(通常这会减少所需的测试总数)
  2. 如果是,你认为重构类有什么设计优势吗?
  3. 如果没有,在你的类中使用测试(使用这个是因为缺少替代)。

我不喜欢友好的方法,因为它改变了被测试的代码,但是测试某些东西的风险,可能与交付的不一样(可能使用第一种方法),将不证明更干净的代码是合理的。

顺便说一句:只测试公共接口也是一件流畅的事情,因为在我的经验中,它的变化和私有实现的变化一样频繁。因此,减少对公共成员的测试没有任何好处。

我自己还没有找到一个黄金解决方案,但是你可以使用friend来测试私有成员,如果你知道测试框架如何命名它的方法。我使用以下方法用Google测试测试私人成员。虽然这工作得很好,但请注意,这是一个hack,我没有在生产代码中使用它。

在我要测试的代码头(stylesheet.h)中,我有:

#ifndef TEST_FRIENDS
#define TEST_FRIENDS
#endif
class Stylesheet {
TEST_FRIENDS;
public:
    // ...
private:
    // ...
};

,在测试中我有:

#include <gtest/gtest.h>
#define TEST_FRIENDS 
    friend class StylesheetTest_ParseSingleClause_Test; 
    friend class StylesheetTest_ParseMultipleClauses_Test;
#include "stylesheet.h"
TEST(StylesheetTest, ParseSingleClause) {
    // can use private members of class Stylesheet here.
}

如果添加一个访问私有成员的新测试,总是在TEST_FRIENDS中添加新行。这种技术的好处是,它在测试代码中相当不引人注目,因为您只添加了几个#define,在不测试时没有任何影响。缺点是在测试中有点冗长。

现在我来解释一下你为什么要这样做。当然,理想情况下,您拥有具有良好定义的职责的小类,并且这些类具有易于测试的接口。然而,在实践中,这并不总是那么容易。如果您正在编写一个库,privatepublic是由您希望库的消费者能够使用的内容(您的公共API)决定的,而不是由需要测试或不需要测试的内容决定的。您可以拥有不太可能更改的不变量,并且需要进行测试,但您的API的消费者对此不感兴趣。那么,对API进行黑盒测试是不够的。此外,如果您遇到bug并编写额外的测试以防止回归,则有必要测试private的内容。

测试私有成员的愿望是一种设计气味,通常表明有一个类被困在您的类中,难以脱身。类的所有功能都应该可以通过它的公共方法来实现;不能公开访问的功能实际上是不存在的。

有几种方法可以让你意识到你需要测试你的私有方法是否像他们在测试中说的那样做。朋友班是其中最糟糕的;它们以一种表面上很脆弱的方式将测试与被测类的实现联系起来。依赖注入稍微好一些:使私有方法的依赖类属性成为测试可以提供模拟版本的属性,以便允许通过公共接口测试私有方法。最好的方法是提取一个类,将私有方法的行为封装为其公共接口,然后像通常那样测试新类。

有关更多详细信息,请参阅Clean Code。

有时需要测试私有方法。测试可以通过向类中添加FRIEND_TEST来完成。

// Production code
// prod.h
#include "gtest/gtest_prod.h"
...   
class ProdCode 
{
    private:
    FRIEND_TEST(ProdTest, IsFooReturnZero);
    int Foo(void* x);
};
//Test.cpp
// TestCode
...
TEST(ProdTest, IsFooReturnZero) 
{
    ProdCode ProdObj;
    EXPECT_EQ(0, ProdObj.Foo(NULL)); //Testing private member function Foo()
}

添加更多信息,因为许多人不知道gtest的功能。

这是来自gtest/gtest_prod.h:

// Copyright 2006, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Google C++ Testing and Mocking Framework definitions useful in production code.
// GOOGLETEST_CM0003 DO NOT DELETE
#ifndef GTEST_INCLUDE_GTEST_GTEST_PROD_H_
#define GTEST_INCLUDE_GTEST_GTEST_PROD_H_
// When you need to test the private or protected members of a class,
// use the FRIEND_TEST macro to declare your tests as friends of the
// class.  For example:
//
// class MyClass {
//  private:
//   void PrivateMethod();
//   FRIEND_TEST(MyClassTest, PrivateMethodWorks);
// };
//
// class MyClassTest : public testing::Test {
//   // ...
// };
//
// TEST_F(MyClassTest, PrivateMethodWorks) {
//   // Can call MyClass::PrivateMethod() here.
// }
//
// Note: The test class must be in the same namespace as the class being tested.
// For example, putting MyClassTest in an anonymous namespace will not work.
#define FRIEND_TEST(test_case_name, test_name)
friend class test_case_name##_##test_name##_Test
#endif  // GTEST_INCLUDE_GTEST_GTEST_PROD_H_

尽管有关于测试私有方法的适当性的注释,假设您真的需要…例如,在将遗留代码重构为更合适的代码之前处理遗留代码时,经常会出现这种情况。下面是我使用的模式:

// In testable.hpp:
#if defined UNIT_TESTING
#   define ACCESSIBLE_FROM_TESTS : public
#   define CONCRETE virtual
#else
#   define ACCESSIBLE_FROM_TESTS
#   define CONCRETE
#endif

然后,在代码中:

#include "testable.hpp"
class MyClass {
...
private ACCESSIBLE_FROM_TESTS:
    int someTestablePrivateMethod(int param);
private:
    // Stuff we don't want the unit tests to see...
    int someNonTestablePrivateMethod();
    class Impl;
    boost::scoped_ptr<Impl> _impl;
}

比定义测试友元好吗?它似乎比另一种方法不那么冗长,并且在标题中清楚地显示正在发生什么。这两种解决方案都与安全性无关:如果您真的关心方法或成员,那么它们需要隐藏在不透明的实现中,可能还有其他保护措施。

在c++中使用#define有一个简单的解决方案。只需要封装你的"ClassUnderTest"这样的:

#define protected public
#define private   public
#include <ClassUnderTest.hpp>
#undef protected
#undef private

[感谢这篇文章和RonFox][1]

另一个不需要在生产代码中添加任何gtest头或宏的解决方案可以是将private成员标记为protected,并创建一个派生类,在测试中使用公开私有成员的公共函数。

待测试类:

#include <cstdint>
class Widget {
protected:
    uint8_t foo;
    uint8_t bar;
    void baz();
}

和测试类:

#include "gtest/gtest.h"
#include <cstdint>
#include "../src/widget.h"
// create a "test" class derived from your production class
class TestWidget : public Widget {
public:
    // create public methods that expose the private members
    uint8_t getFoo() {
        return foo;
    }
    void callBar() {
        bar();
    }
}
TEST(WidgetTests, TestFoo) {
    // Use the derived class in your tests
    TestWidget widget;
    // call a protected method
    widget.callBar();
    // check the value of a protected member
    EXPECT_EQ(widget.getFoo(), 10);
}

当然,这里的主要警告是,您必须将需要访问的private成员标记为protected,但如果您想保留"test"

我认为一个更好的解决方案是实现一个代码生成器。当提供类名或包含要测试的类的目录路径时,它应该扫描源代码,识别私有数据,并使用生成的源代码生成一个新的临时目录。然后,测试应该使用该类或该路径来执行测试。

如果可能的话,

必须保留成员布局;否则,测试将是多余的。

这样做的优点是不会入侵源代码本身,并且生成的目录可以被版本控制安全地忽略。不需要反射

缺点是该目录的大小与输入源相同或更多,因此需要高覆盖率的大型项目可能会发现它很麻烦。

那么我们在这些新文件中做什么呢?

  1. 将测试类作为友类插入。测试可以大量访问类内部。可能会扰乱数据布局(?)。
  2. 使private变成public。与上面相同,但可能会扰乱成员数据布局。
  3. 为类插入一些朋友或成员访问器(我不确定是否应该使用mutator,因为除非进行压力测试,否则可能不会有用)。测试只能访问可见的内容,并且在像c++这样不能单独访问成员的语言中,这允许对数据进行细粒度的展示,而不必弄乱数据布局(?? ?)。
最后,我们将得到像虚幻引擎的Header工具一样的东西,它将自定义的宏类元数据解析成样板或生成的代码,并添加到项目中。

我更喜欢在单元测试的Makefile中添加-Dprivate=public选项,避免修改我原始项目中的任何内容