52

我想为我的 C++ 应用程序进行单元测试。

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

测试 API 使用哪种技术?

4

8 回答 8

50

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

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

允许单元测试访问非公共成员的一种方法是通过朋友构造。

于 2013-01-06T20:40:53.960 回答
27

回答这个问题涉及许多其他话题。除了 CleanCode、TDD 和其他方面的宗教信仰之外:

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

全部定义为公开

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

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

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

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

这意味着,编译器有权为测试重新排序成员变量和虚函数。如果没有发生缓冲区溢出,您可能会感到困惑,这不会损害您的类,但这意味着您不会在交付时测试相同的代码。这意味着,如果您访问由代码初始化、编译时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
};

我认为最后两种方式都不能替代前两种方式,因为它们与第一种方式相比没有优势,但对测试代码的侵入性更大。第一种方式风险很大,所以你可以使用交友方式。


关于从不测试私人事物讨论的一些话。单元测试的好处之一是,你会很早就达到你必须改进代码设计的地步。这有时也是单元测试的缺点之一。它使面向对象有时比它必须的更复杂。特别是如果您遵循规则以与现实世界对象相同的方式设计类。

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

课程有时会变得复杂!然后你必须决定分开它们还是照原样接受它们。有时第二个决定更有意义。归根结底,您想要实现的目标始终是一个问题(例如,完美的设计、快速的整合时间、低开发成本……)。


我的意见

我访问私人成员的决策过程如下所示:

  1. 您需要自己测试私人成员吗?(通常这会减少所需的测试总数)
  2. 如果是,您是否看到重构该类的任何设计优势?
  3. 如果不是,请在您的班级中与测试成为朋友(因为缺少替代方案而使用此测试)。

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

顺便说一句:仅测试公共接口也是一个流畅的问题,因为根据我的经验,它的变化与私有实现一样频繁。所以你没有优势减少对公共成员的测试。

于 2014-06-27T06:32:52.743 回答
23

我自己还没有找到黄金解决方案,但是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 中添加一个新行。这种技术的好处是它在测试代码中相当不显眼,因为您只添加了几个#defines,在不测试时它们没有效果。缺点是它在测试中有点冗长。

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

于 2013-01-06T20:54:30.777 回答
4
Sometimes, it is required to test private methods. Testing can be done by adding FRIEND_TEST to the class.

    // 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()

    }

Adding more info, since many are not aware of gtest features.
This is from gtest/gtest_prod.h

https://github.com/google/googletest/blob/master/googletest/include/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_
于 2017-12-13T09:38:19.213 回答
3

测试私有成员的愿望是一种设计气味,通常表明有一个班级被困在你的班级里,正在努力摆脱。一个类的所有功能都应该可以通过它的公共方法来执行;无法公开访问的功能实际上并不存在。

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

有关更多详细信息,请参阅清洁代码

于 2013-01-10T17:06:33.743 回答
3

尽管有关于测试私有方法的适当性的评论,但假设您确实需要......例如,在将遗留代码重构为更合适的东西之前,这通常是这种情况。这是我使用的模式:

// 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;
}

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

于 2014-01-16T16:18:47.830 回答
0

C++ 中有一个使用#define 的简单解决方案。只需像这样包装您的“ClassUnderTest”的包含:

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

[归功于这篇文章和 RonFox][1]

于 2015-12-16T17:45:35.110 回答
-4

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

于 2016-04-29T03:21:41.940 回答