17

今日、私は同僚と、クラスのプライベートメンバーまたはプライベートステートをテストするかどうかについて話し合いました。彼はそれが理にかなっている理由を私にほとんど確信させました。この質問は、プライベートメンバーをテストする性質と理由に関する既存のStackOverflowの質問を複製することを目的としていません。たとえば、ユニットテストをテストしているクラスの友達にすることの何が問題になっていますか。

私の意見では、同僚の提案は、単体テストの実装クラスにフレンド宣言を導入するのに少し脆弱でした。私の意見では、テストされたコードの依存関係をテストコードに導入しているのに対し、テストコードはすでにテストされたコード=>循環依存関係に依存しているため、これは問題ありません。テストクラスの名前を変更するような無邪気なことでさえ、単体テストを壊し、テストされたコードのコード変更を強制します。

テンプレート関数を特殊化することが許可されているという事実に依存する他の提案について、C++の達人に判断してもらいたいと思います。クラスを想像してみてください。

// tested_class.h

struct tested_class 
{
  tested_class(int i) : i_(i) {}

  //some function which do complex things with i
  // and sometimes return a result

private:
  int i_;
};

テスト可能にするためだけにi_のゲッターを用意するというアイデアは好きではありません。したがって、私の提案は、クラスでの「test_backdoor」関数テンプレート宣言です。

// tested_class.h

struct tested_class 
{
  explicit
  tested_class(int i=0) : i_(i) {}

  template<class Ctx>
  static void test_backdoor(Ctx& ctx);

  //some function which do complex things with i
  // and sometimes return a result

private:
  int i_;
};

この関数を追加するだけで、クラスのプライベートメンバーをテスト可能にすることができます。単体テストクラスやテンプレート関数の実装への依存関係はないことに注意してください。この例では、単体テストの実装はBoostTestフレームワークを使用しています。

// tested_class_test.cpp

namespace
{
  struct ctor_test_context
  {
    tested_class& tc_;
    int expected_i;
  };
}

// specialize the template member to do the rest of the test
template<>
void tested_class::test_backdoor<ctor_test_context>(ctor_test_context& ctx)
{
  BOOST_REQUIRE_EQUAL(ctx.expected_i, tc_.i_);
}

BOOST_AUTO_TEST_CASE(tested_class_default_ctor)
{
  tested_class tc;
  ctor_test_context ctx = { tc, 0 };
  tested_class::test_backdoor(ctx);
}

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  tested_class tc(-5);
  ctor_test_context ctx = { tc, -5 };
  tested_class::test_backdoor(ctx);
}

まったく呼び出せない単一のテンプレート宣言を導入することで、テスト実装者にテストロジックを関数に転送する可能性を与えます。この関数は、タイプセーフコンテキストに作用し、テストコンテキストの匿名タイプの性質により、特定のテストコンパイルユニット内からのみ表示されます。そして最良のことは、テストされたクラスに触れることなく、好きなだけ匿名のテストコンテキストを定義し、それらのテストに特化できることです。

確かに、ユーザーはテンプレートの特殊化が何であるかを知っている必要がありますが、このコードは本当に悪いか、奇妙であるか、読めませんか?または、C ++開発者に、C ++テンプレートの特殊化とは何か、およびそれがどのように機能するかについての知識があることを期待できますか?

友達を使ってユニットテストクラスを宣言することについて詳しく説明しますが、これは堅牢ではないと思います。ブーストフレームワーク(または他のテストフレームワーク)を想像してみてください。テストケースごとに個別のタイプを生成します。しかし、私が書くことができる限り、なぜ私は気にする必要があります:

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  ...
}

フレンドを使用する場合は、各テストケースをフレンドとして宣言する必要がありました...または、一般的なタイプ(フィクスチャなど)でテスト機能を導入し、フレンドとして宣言して、すべてのテスト呼び出しをそのタイプに転送することになります。 。それは奇妙ではありませんか?

私はあなたの賛否両論がこのアプローチを実践しているのを見たいと思います。

4

9 に答える 9

21

単体テストとは、テスト対象のクラスの観察可能な動作をテストすることだと思います。したがって、プライベート パーツ自体は観察できないため、プライベート パーツをテストする必要はありません。オブジェクトをテストする方法は、オブジェクトが期待どおりに動作するかどうかをテストすることです (これは、すべてのプライベートな内部状態が正常であることを暗黙のうちに意味します)。

プライベート パーツについて心配する必要がない理由は、このようにして、テストを書き直すことなく実装を変更 (リファクタリングなど) できるからです。

したがって、私の答えは、単体テストの哲学に反するため、(技術的に可能であっても)実行しないことです。

于 2012-07-17T01:28:36.583 に答える
6

長所

  • プライベートメンバーにアクセスしてテストできます
  • そのかなり最小限の量hack

短所

  • 壊れたカプセル化
  • より複雑で、同じくらいもろい壊れたカプセル化friend
  • test_backdoor本番側に置くことによる本番コードとの混合テスト
  • メンテナンスの問題(テストコードを友だちにするのと同じように、テストコードと非常に緊密な結合を作成しました)

すべての長所/短所は別として、複雑なことが起こっている場合は、より良いテストを可能にするアーキテクチャの変更を行うのが最善だと思います。

可能な解決策

  • Pimplイディオムを使用complexし、プライベートメンバーと一緒にコードをpimplに配置し、Pimplのテストを記述します。Pimplは、パブリックメンバーとして前方宣言できるため、単体テストで外部インスタンス化できます。Pimplはパブリックメンバーのみで構成できるため、テストが容易になります
    • 短所:たくさんのコード
    • 短所:デバッグ時に内部が見えにくくなる不透明(OPAQUE)型
  • クラスのパブリック/保護されたインターフェースをテストするだけです。インターフェイスがレイアウトするコントラクトをテストします。
    • 短所:単体テストを分離して記述することは困難/不可能です。
  • Pimplソリューションに似ていますが、complexコードを含む無料の関数を作成します。宣言をプライベートヘッダー(ライブラリのパブリックインターフェイスの一部ではない)に配置し、テストします。
  • 友人を介してカプセル化を破る試験方法/備品
    • これの可能なバリエーション:宣言しfriend struct test_context;、テストコードをの実装のメソッド内に配置しますstruct test_context。このようにして、各テストケース、メソッド、またはフィクスチャを友だちにする必要はありません。これにより、誰かが友情を壊す可能性を減らすことができます。
  • テンプレートの特殊化によるカプセル化の解除
于 2012-07-17T04:39:30.807 に答える
2

「フレンド」機能を引き続き使用するため、技術的にはあなたの質問に対する直接的な答えではありませんが、テストされたエンティティ自体を変更する必要はなく、一部で言及されているカプセル化を破る懸念が追加されると思います他の答えの; ただし、ボイラープレート コードを記述する必要があります。

その背後にあるアイデアは私のものではなく、実装は 完全に彼のブログで litb によって提示および説明されているトリックに基づいています(少なくとも私にとっては、このSutter's gotwと組み合わせて、もう少しコンテキストを追加します) - 簡単に言えば、CRTP、友人、 ADL とメンバーへのポインタ (残念なことに、ADL の部分がまだ完全には理解できていないことを告白しなければなりませんが、100% 解明するために懸命に取り組んでいます)。

gcc 4.6、clang 3.1、および VS2010 コンパイラでテストしたところ、完全に動作しました。

/* test_tag.h */
#ifndef TEST_TAG_H_INCLUDED_
#define TEST_TAG_H_INCLUDED_

template <typename Tag, typename Tag::type M>
struct Rob
{
    friend typename Tag::type get(Tag)
    {
        return M;
    }
};

template <typename Tag, typename Member> 
struct TagBase
{
    typedef Member type;
    friend type get(Tag);
};


#endif /* TEST_TAG_H_INCLUDED_ */

/* tested_class.h */
#ifndef TESTED_CLASS_H_INCLUDED_
#define TESTED_CLASS_H_INCLUDED_

#include <string>

struct tested_class
{
    tested_class(int i, const char* descr) : i_(i), descr_(descr) { }

private:
    int i_;
    std::string descr_;
};

/* with or without the macros or even in a different file */
#   ifdef TESTING_ENABLED
#   include "test_tag.h"

    struct tested_class_i : TagBase<tested_class_i, int tested_class::*> { };
    struct tested_class_descr : TagBase<tested_class_descr, const std::string tested_class::*> { };

    template struct Rob<tested_class_i, &tested_class::i_>;
    template struct Rob<tested_class_descr, &tested_class::descr_>;

#   endif

#endif /* TESTED_CLASS_H_INCLUDED_ */

/* test_access.cpp */
#include "tested_class.h"

#include <cstdlib>
#include <iostream>
#include <sstream>

#define STRINGIZE0(text) #text
#define STRINGIZE(text) STRINGIZE0(text)

int assert_handler(const char* expr, const char* theFile, int theLine)
{
    std::stringstream message;
    message << "Assertion " << expr << " failed in " << theFile << " at line " << theLine;
    message << "." << std::endl;
    std::cerr << message.str();

    return 1;
}

#define ASSERT_HALT() exit(__LINE__)

#define ASSERT_EQUALS(lhs, rhs) ((void)(!((lhs) == (rhs)) && assert_handler(STRINGIZE((lhs == rhs)), __FILE__, __LINE__) && (ASSERT_HALT(), 1)))

int main()
{
    tested_class foo(35, "Some foo!");

    // the bind pointer to member by object reference could
    // be further wrapped in some "nice" macros
    std::cout << " Class guts: " << foo.*get(tested_class_i()) << " - " << foo.*get(tested_class_descr()) << std::endl;
    ASSERT_EQUALS(35, foo.*get(tested_class_i()));
    ASSERT_EQUALS("Some foo!", foo.*get(tested_class_descr()));

    ASSERT_EQUALS(80, foo.*get(tested_class_i()));

    return 0; 
}
于 2012-07-24T00:05:12.547 に答える
1

私は通常、プライベート メンバーと関数を単体テストする必要性を感じません。正しい内部状態を確認するためだけにパブリック関数を導入することを好むかもしれません。

しかし、詳細を調べることにした場合は、単体テスト プログラムで厄介なクイック ハックを使用します。

#include <system-header>
#include <system-header>
// Include ALL system headers that test-class-header might include.
// Since this is an invasive unit test that is fiddling with internal detail
// that it probably should not, this is not a hardship.

#define private public
#include "test-class-header.hpp"
...

少なくとも Linux では、C++ 名マングリングにはプライベート/パブリック状態が含まれないため、これは機能します。他のシステムではこれは正しくない可能性があり、リンクしないと言われています。

于 2012-07-12T18:31:06.760 に答える
1

最初に質問することは、なぜ友人は注意して使用する必要があると見なされるのかということです。

カプセル化を破るためです。オブジェクトの内部にアクセスできる別のクラスまたは関数を提供するため、プライベート メンバーの可視スコープが拡張されます。友達がたくさんいる場合、オブジェクトの状態を判断するのははるかに困難です。

私の意見では、テンプレート ソリューションは、その点で友人よりもさらに悪いです。テンプレートの主な利点は、クラスからテストを明示的にフレンド登録する必要がなくなったことです。それどころか、これは不利益であると私は主張します。それには2つの理由があります。

  1. テストは、クラスの内部に結合されています。クラスを変更する人は誰でも、オブジェクトのプライベートを変更することで、テストに違反する可能性があることを知っておく必要があります。友人は、どのオブジェクトがクラスの内部状態に結合されている可能性があるかを正確に伝えますが、テンプレート ソリューションはそうではありません。

  2. フレンドはプライベートの範囲拡大を制限します。クラスを友達にすると、そのクラスだけが内部にアクセスできることがわかります。したがって、テストを友達にすると、プライベート メンバー変数の読み取りまたは書き込みができるのはテストだけであることがわかります。ただし、テンプレートのバックドアはどこでも使用できます。

テンプレート ソリューションは、問題を修正するのではなく隠してしまうため、効果がありません。クラスを変更する人はバックドアのすべての使用について知っている必要があり、テストを変更する人はクラスについて知っている必要があります。基本的に、クラスからのテストへの参照は、すべてのプライベート データをパブリック データにすることによってのみ削除されました。

テストからプライベート メンバーにアクセスする必要がある場合は、テスト フィクスチャをフレンド登録するだけで完了です。シンプルでわかりやすいです。

于 2012-07-21T09:08:09.470 に答える
1

プライベート メンバーのテストは、期待される値と等しいかどうかをチェックして状態を検証することだけではありません。他のより複雑なテスト シナリオに対応するために、次のアプローチを使用することがあります (ここでは主なアイデアを伝えるために簡略化しています)。

// Public header
struct IFoo
{
public:
    virtual ~IFoo() { }
    virtual void DoSomething() = 0;
};
std::shared_ptr<IFoo> CreateFoo();

// Private test header
struct IFooInternal : public IFoo
{
public:
    virtual ~IFooInternal() { }
    virtual void DoSomethingPrivate() = 0;
};

// Implementation header
class Foo : public IFooInternal
{
public:
    virtual DoSomething();
    virtual void DoSomethingPrivate();
};

// Test code
std::shared_ptr<IFooInternal> p =
    std::dynamic_pointer_cast<IFooInternal>(CreateFoo());
p->DoSomethingPrivate();

このアプローチには、優れた設計を促進し、フレンド宣言が煩雑にならないという明確な利点があります。もちろん、最初からプライベート メンバーをテストできることはかなり非標準的な要件であるため、ほとんどの場合、問題を経験する必要はありません。

于 2012-07-12T18:50:40.627 に答える
1

TestInvariant() と呼ばれる関数を使用して、プライベート クラス メンバーをテストしました。

これはクラスのプライベート メンバーであり、デバッグ モードでは、すべての関数の最初と最後で呼び出されました (ctor の最初と dctor の最後を除く)。

それは仮想であり、独自のバージョンになる前に親バージョンと呼ばれる基本クラスでした。

これにより、クラスの内部を誰にも公開することなく、常にクラスの内部状態を確認することができました。私は非常に単純なテストをしましたが、複雑なものを作成したり、フラグなどでオンまたはオフに設定したりできない理由はありません.

また、TestInvariant() 関数を呼び出す他のクラスから呼び出すことができるパブリック Test 関数を使用することもできます。したがって、内部クラスの動作を変更する必要がある場合、ユーザー コードを変更する必要はありません。

これは役に立ちますか?

于 2012-07-20T15:12:22.727 に答える
0

プライベートな場合は単独でテストするべきではなく、必要に応じて再設計する必要があるという理論があります。

私にとってそれがシーア派です。

一部のプロジェクトでは、プライベート メソッド用のマクロを次のように作成します。

class Something{
   PRIVATE:
       int m_attr;
};

テスト用にコンパイルする場合、PRIVATE は public として定義されます。それ以外の場合は、private として定義されます。それは簡単です。

于 2012-07-20T17:50:16.420 に答える