【问题标题】:Unit testing c++. How to test private members?单元测试 C++。如何测试私人会员?
【发布时间】:2012-12-20 14:37:37
【问题描述】:

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

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

测试 API 使用哪种技术?

【问题讨论】:

  • 通过单元测试,您正在测试界面的行为。所以你不应该关心对象的内部状态
  • 在 C++ 中,您始终可以使用 #define private public#define class struct,然后就没有什么是私有的了!
  • 很遗憾我们不能对评论投反对票。 @BeniBela 我希望您意识到您的建议是非常糟糕的编码实践。不过很有趣。
  • 但是测试私有成员的正确方法是什么?它们必须经过测试,对吗?
  • @jimmy_keen 我同意单元测试是为了测试“合同”。但是,您的代码的某些部分可能受(内部)“合同”管辖,您不希望将这些部分公开给代码的用户。 publicprivate 主要是针对你的代码的消费者的访问控制,而不一定是为了区分契约治理和非契约治理。

标签: c++ unit-testing testing


【解决方案1】:

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

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

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

【讨论】:

  • +1 终于得到了一个没有说“不要测试私有方法”的答案。即使糟糕的工程师不这么认为,包容也并不总是有效。
  • 是的,我对这个网站上的众多人感到困惑,他们明确表示编写未经直接测试的代码不仅没问题,而且可取——即, 私人会员。有时没有很好的方法来处理非平凡的私有函数,有时很难通过仅调用公共接口来测试这些私有函数中的所有边缘情况。单元测试的真正目的是让代码更好,而不是一些关于区分公共和私有接口的废话。在这种情况下,不测试私人成员是不专业的。
  • 在某些领域,如安全关键型工业设备和医疗设备开发法规,迫使您广泛测试私有和受保护的成员功能......尽管从功能角度这样做是否有意义查看。
【解决方案2】:

回答这个问题涉及许多其他主题。除了 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. 如果没有,请与您班上的测试成为朋友(使用此测试,因为缺少替代方案)。

我不喜欢交友方法,因为它会更改已测试的代码,但测试某些东西的风险可能与交付的不一样(与第一种方法一样),并不能证明更简洁的代码是合理的。

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

【讨论】:

  • #define private public 不是解决方案,因为它可以更改类的布局,并使其与您尝试测试的代码不兼容。正式地,这被称为违反单一定义规则。 (您会测试编译器输出,而不仅仅是源代码,对吗?即使只测试源代码,您现在也不会测试相同的源代码。)
  • 这不是我担心的 ODR。顺序的改变更为重要。我想这个应该大家都知道,谁推荐#define private public。我喜欢 C++,因为即使经过 10 年的使用经验,你也能学到一些东西。谢谢。
  • 你真的应该删除关于“优点是,它不是侵入性的,因此对测试代码是被动的”部分,因为它是非常错误的。为准确性而编辑答案是错误的吗?
  • 我显然忽略了这一点。所以我把那句话删了。谢谢。
  • 有一种方法可以简单地访问私有而不侵入或修改代码,如此处所述bloglitb.blogspot.com.es/2010/07/…
【解决方案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 中添加一个新行。这种技术的好处是它在测试代码中相当不显眼,因为您只添加了几个#defines,在不测试时它们没有效果。缺点是在测试中有点冗长。

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

【讨论】:

  • 我知道这已经过时了,但我只是在使用 gtest 并在调用私人成员时搞砸了,并且想更新 gtest 对此的建议:Testing Private Members。最低限度,您现在可以使用 FRIEND_TEST(TestCaseName, TestName) 将测试声明为朋友,而不是依赖命名约定。确保生产安全! (必须#include "gtest/gtest_prod.h")。
【解决方案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_

【讨论】:

【解决方案5】:

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

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

更多详情,请咨询Clean Code

【讨论】:

  • 只有一个 Add() 方法的接口呢?您如何测试添加是否完成?还是通过预期的逻辑/后备行为等来完成?
  • @user3063349 如果唯一的接口是Add 方法,那么没有人会知道它是否不起作用,因此您无需对其进行测试。你也不需要那个方法或那个类,并且可以享受删除一堆死代码的独特乐趣。
  • 链接到“干净的代码”只会让这个答案变得更糟
【解决方案6】:

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

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

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

【讨论】:

    【解决方案7】:

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

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

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

    【讨论】:

      【解决方案8】:

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

      【讨论】:

      • 它有效,但肯定有一些陷阱。
      猜你喜欢
      • 2011-04-22
      • 1970-01-01
      • 2011-05-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-01
      • 1970-01-01
      相关资源
      最近更新 更多