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


35

私は最近、同じクラス内のメソッドから同じクラス内のメソッドへの結果または呼び出しをスタブするのに最適な次の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はパブリックにすることもできます


あなたの例は単純ですが、正確ではありません。FunctionAboolを返しますが、ローカル変数のみを設定し、x何も返しません。
エリックP.

1
この特定の例では、FunctionBをpublic static別のクラスにすることができます。

コードレビューの場合は、簡略化されたバージョンではなく、実際のコードを投稿する必要があります。よくある質問をご覧ください。現状では、コードレビューを探しているのではなく、特定の質問をしています。
ウィンストンユワート

1
FunctionB設計により破損しています。new Random().Next()ほとんど常に間違っています。のインスタンスを注入する必要がありますRandom。(Randomまた、いくつかの追加の問題を引き起こす可能性のある
不適切に

代表者を経由して、より一般的なノートDIに絶対に罰金私見です
JK。

回答:


32

オリジナルのポスター更新後に編集。

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

長いバージョンは:private / protectedメソッドはAPIの一部ではなく、基本的に実装の選択肢であり、外部に影響を与えることなくレビュー、更新、または完全に破棄することを決定できます。

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

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

コメント続いて、FunctionBがパブリックの場合、ユニットテストを使用して両方をテストします。FunctionAのテストは完全に「ユニット」ではないと考えるかもしれませんが(FunctionBを呼び出すので)、私はあまり心配しません。 FunctionBのサブドメイン。これは、識別者として十分です。

2つのテストを完全に分離できるようにしたい場合は、FunctionAをテストするときに、何らかの種類のモック手法を使用してFunctionBをモックします(通常、修正済みの既知の正しい値を返します)。特定のモックライブラリをアドバイスするC#エコシステムの知識はありませんが、この質問をご覧ください


2
@Martinの回答に完全に同意します。クラスの単体テストを作成するときは、メソッドをテストしないでください。テストしているのは、クラスの動作であり、コントラクト(クラスが行うべき宣言)が満たされていることです。そのため、ユニットテストは、例外クラスを含む(パブリックメソッド/プロパティを使用して)このクラスに公開されているすべての要件をカバーする必要があります

こんにちは、回答ありがとうございます。しかし、私の質問には答えませんでした。FunctionBがプライベートであるか保護されているかは関係ありません。また、パブリックにすることも、FunctionAから呼び出すことができます。

基本クラスを再設計せずにこの問題を処理する最も一般的な方法は、サブクラス化MyClassし、スタブ化する機能でメソッドをオーバーライドすることです。また、質問を更新して、FunctionB公開される可能性のある質問を含めることをお勧めします。
エリックP.

1
protectedメソッドは、異なるアセンブリにクラスの実装が存在しないことを確認しない限り、クラスのパブリックサーフェスの一部です。
-CodesInChaos

2
FunctionAがFunctionBを呼び出すという事実は、単体テストの観点からは無関係な詳細です。FunctionAのテストが正しく記述されている場合、テストを中断することなく後でリファクタリングできる実装の詳細です(FunctionAの全体的な動作が変更されない限り)。本当の問題は、FunctionBの乱数の取得は、注入されたオブジェクトで行う必要があるため、テスト中にモックを使用して、既知の数値が返されることを確認できることです。これにより、既知の入力/出力をテストできます。
ダンライオンズ

11

関数がテストすることが重要である、または置換することが重要である場合、テスト対象のクラスのプライベート実装の詳細ではなく、別のクラスのパブリック実装の詳細であることが重要であるという理論に同意します。

だから私が持っているシナリオにいるなら

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()が独立してテスト可能であり、完全に置き換え可能なシナリオを持っています。

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


ICollaboratorにいくつかの方法が必要な場合は可能です。単一のメソッドをラップするだけの仕事をしているオブジェクトがある場合は、デリゲートに置き換えてください。
jk。

名前付きデリゲートが理にかなっているか、インターフェイスが理にかなっているかを決定する必要があり、私はあなたのために決定しません。個人的に、私は単一の(パブリック)メソッドクラスを嫌いではありません。小さくなればなるほど、理解しやすくなります。
アンソニー

0

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

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

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

方法1-これにより、テスト中のテスト対象クラスの動作が実際に変更されます。

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

記載されている問題では、Aの唯一のロジックは、FunctionBの出力が偶数かどうかを確認することです。説明のためですが、FunctionBはランダム値を提供しますが、これはテストするのが困難です。

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


3
protectedはとほぼ同じpublicです。のみprivateinternal実装の詳細です。
CodesInChaos

@codeinchaos-ここで興味があります。テストでは、アセンブリ属性を変更しない限り、保護されたメソッドは「プライベート」です。派生型のみが保護されたメンバーにアクセスできます。virtualを除いて、なぜ保護されたものがテストからパブリックと同様に扱われるべきなのかわかりません。詳しく説明してもらえますか?
スリカンスベヌゴパラン

これらの派生クラスは異なるアセンブリに含まれている可能性があるため、サードパーティのコードにさらされ、クラスのパブリックサーフェスの一部になります。それらをテストするには、それらをinternal protected作成するか、プライベートリフレクションヘルパーを使用するか、テストプロジェクトで派生クラスを作成します。
CodesInChaos

@CodesInChaosは、派生クラスを異なるアセンブリに含めることができることに同意しましたが、スコープは依然として基本型と派生型に制限されています。アクセス修飾子をテスト可能にするために変更することは、少し不安です。私はそれをやったが、それは私にとってアンチパターンのようです。
スリカンスヴェヌゴパラン

0

私は個人的に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. 最大の欠点は、FuncsおよびActionsの 'this'参照にアクセスできないことです。したがって、クラスへの依存関係は、パラメーターとして明示的に渡す必要があります。すべての依存関係を知っているのは良いことですが、Funcが依存する多くのプロパティがある場合は悪いことです。マイレージはユースケースによって異なります。

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

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

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

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

お役に立てれば!


-1

Mockを使用することは可能です。Nuget:https ://www.nuget.org/packages/moq/

そして、私はそれがかなりシンプルで理にかなっていると信じています。

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Mockはオーバーライドするために仮想メソッドを必要とします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.