web-dev-qa-db-ja.com

同じクラス内の他のメソッドを呼び出すメソッドを単体テストする最良の方法

最近、友人と、次の2つのメソッドのうち、返される結果または同じクラス内のメソッドから同じクラス内のメソッドへの呼び出しをスタブ化するのに最適なメソッドについて話し合いました。

これは非常に単純化された例です。実際には、機能ははるかに複雑です。

例:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

これをテストするには、2つの方法があります。

方法1:関数とアクションを使用して、メソッドの機能を置き換えます。例:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

方法2:メンバーを仮想にしてクラスを派生させ、派生クラスで関数とアクションを使用して機能を置き換えます例:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

私はどちらが良いのか、そしてなぜなぜ知りたいのですか?

更新:注:FunctionBもパブリックにすることができます

36
tranceru1

オリジナルのポスター更新に続いて編集されました。

免責事項:C#プログラマーではない(主にJavaまたはRuby)。私の答えは次のとおりです。私はまったくテストしません。そうすべきではないと思います。

長いバージョンは:プライベート/保護されたメソッドはAPIの一部ではなく、基本的に実装の選択であり、外部に影響を与えることなく、確認、更新、または完全に破棄することができます。

外部の世界から見えるクラスの一部であるFunctionA()のテストがあると思います。これは、実装する契約を持っている(そしてテストできる)唯一のものでなければなりません。あなたのプライベート/保護されたメソッドは、実行および/またはテストするための契約がありません。

そこで関連ディスカッションを参照してください: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

コメントに従って、FunctionBがパブリックの場合は、単体テストを使用して両方をテストします。 FunctionAのテストは完全に「ユニット」ではない(FunctionBを呼び出すため)と思うかもしれませんが、それほど心配する必要はありません。FunctionBテストが機能し、FunctionAテストが機能しない場合、問題は明らかにFunctionBのサブドメイン。これは、私にとって弁別子としては十分です。

2つのテストを完全に分離できるようにしたい場合は、FunctionAをテストするときに、ある種のモック手法を使用してFunctionBをモックします(通常、既知の固定された正しい値を返します)。特定のモックライブラリに助言するC#エコシステムの知識が不足していますが、 この質問 を参照してください。

32
Martin

私は、関数がテストにとって重要であるか、または置き換えることが重要である場合、テスト中のクラスのプライベート実装の詳細ではなく、のパブリック実装の詳細であることが十分に重要であるという理論に同意します。違うクラス。

だから私が持っているシナリオでは

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

次に、リファクタリングします。

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

今私はD()が独立してテスト可能で完全に置き換え可能であるシナリオを持っています。

整理の手段として、私のコラボレーターmightは同じ名前空間レベルに住んでいません。たとえば、AがFooCorp.BLLにある場合、コラボレーターはFooCorp.BLL.Collaborators(または適切な名前)のように、別の深層になる可能性があります。さらに、コラボレーターはinternalアクセス修飾子を介してアセンブリー内にのみ表示され、InternalsVisibleToアセンブリー属性を介してユニットテストプロジェクトにも公開します。要点は、検証可能なコードを生成しながら、呼び出し元に関する限り、APIをクリーンに保つことができるということです。

11
Anthony Pegram

私はMethod1を個人的に使用しています。つまり、すべてのメソッドをActionsまたはFuncsにして、コードのテスト性を大幅に向上させています。他のソリューションと同様に、このアプローチには長所と短所があります。

長所

  1. 単純なコード構造が可能になり、単体テストのためだけにコードパターンを使用すると、さらに複雑になります。
  2. クラスをシールしたり、Moqなどの一般的なモックフレームワークに必要な仮想メソッドを排除したりできます。クラスをシーリングして仮想メソッドを削除すると、インライン化やその他のコンパイラ最適化の候補になります。 ( https://msdn.Microsoft.com/en-us/library/ff647802.aspx
  3. ユニットテストでのFunc/Action実装の置き換えは、Func/Actionに新しい値を割り当てるだけで簡単なので、テスト容易性を簡素化します。
  4. 静的メソッドはモックできないため、別のメソッドから静的Funcが呼び出されたかどうかをテストすることもできます。
  5. 呼び出しサイトでメソッドを呼び出す構文は同じであるため、既存のメソッドをFuncs/Actionsに簡単にリファクタリングできます。 (メソッドをFunc/Actionにリファクタリングできない場合は、短所を参照してください)

短所

  1. Funcs/Actionsにはメソッドのような継承パスがないため、クラスを派生できる場合はFuncs/Actionsを使用できません。
  2. デフォルトのパラメータは使用できません。デフォルトのパラメーターでFuncを作成するには、新しいデリゲートを作成する必要があり、ユースケースによってはコードが混乱する可能性があります。
  3. 何か(firstName: "S"、lastName: "K")などのメソッドを呼び出すために名前付きパラメーター構文を使用することはできません
  4. 最大の欠点は、FuncおよびActionsの「this」参照にアクセスできないため、クラスへの依存関係をパラメーターとして明示的に渡す必要があることです。あなたはすべての依存関係を知っているので良いですが、あなたのFuncが依存する多くのプロパティがある場合は悪いです。走行距離はユースケースによって異なります。

つまり、クラスがオーバーライドされないことがわかっている場合は、ユニットテストにFuncsとActionsを使用することをお勧めします。

また、私は通常Funcsのプロパティを作成しませんが、そのように直接インライン化します

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

お役に立てれば!

0

マーティンの指摘に加えて、

メソッドがプライベート/保護されている場合-テストしないでください。これはクラスの内部にあり、クラスの外部からアクセスしないでください。

あなたが言及している両方のアプローチで、私はこれらの懸念を持っています-

方法1-これは実際に、テスト中のクラスのテストでの動作を変更します。

方法2-これは実際には製品コードをテストせず、代わりに別の実装をテストします。

述べた問題では、Aの唯一のロジックはFunctionBの出力が偶数かどうかを確認することです。例証ではありますが、FunctionBはランダムな値を提供するため、テストは困難です。

私は、FunctionBが何を返すかがわかるようにMyClassをセットアップできる現実的なシナリオを期待しています。次に、予想される結果がわかったら、FunctionAを呼び出して、実際の結果をアサートできます。