web-dev-qa-db-ja.com

実装の詳細をテストに公開する

私は、内部状態についての深い知識なしにテストするのが難しい動作を実装するクラスを作成しました。この内部状態のアクセサーでクラスのパブリックAPIを散らかしたくないし、この内部状態に応じてコードを作成している他のプログラマーにも多少の摩擦を加えたいです。それで、次のことをするのは良い考えでしょうか?

_// SUT:
public class ComplicatedThing
{
    // The important behavior
    public void DoStuff() { ... }

    // Things necessary for tests:
    public interface ITestClient
    {
        int IntimateKnowledge { set; }
    }
    public void Accept(ITestClient client)
    {
        client.IntimateKnowledge = ...;
    }
}

// Tests:
public class ComplicatedThingClass
{
    class Client : ComplicatedThing.ITestClient
    {
        public int IntimateKnowledge { get; set; } = -1;
    }

    public class DoStuffMethodShould
    {
        [Fact]
        public void DoComplicatedThings()
        {
            var thing = new ComplicatedThing();

            thing.DoStuff(...);

            var client = new Client();
            thing.Accept(client);
            Assert.Equal(123, client.IntimateKnowledge);
        }
    }
}
_

細部

具体的には、オブジェクトの終了を非同期で検出できるクラスがあります。

_public class LifetimeWatcher
{
    private readonly ConditionalWeakTable<object, HashSet<...>> _weakTable = ConditionalWeakTable<object, HashSet<...>>();

    private int ResidueLevel => _weakTable.Select(kvp => kvp.Value.Count + 1).Sum();

    public async ValueTask WaitForFinalizationAsync(
        WeakReference<object> weakTarget,
        CancellationToken cancellationToken);
}
_

テストしたいことの1つは、WaitForFinalizationAsyncの呼び出しが完了しても「残差」が残っていないことです。具体的には、private ConditionalWeakTableに追加されたエントリは、関数が戻るときに削除する必要があります。これは、ResidueLevelプロパティで簡単に数量化できます。

しかし、パブリックAPIを乱雑にしたり、この実装の詳細に応じて始まるコードを他の人が簡単に作成したりせずに、その情報をテストで利用できるようにするにはどうすればよいですか?

他に何をすべきか本当にわからないので、クラスに以下を追加しました:

_public class LifetimeWatcher
{
    // ...

    public interface ITestClient
    {
        int ResidueLevel { set; }
    }
    public Accept(ITestClient client)
    {
        client.ResidueLevel = this.ResidueLevel;
    }
}
_

通常のテストは、通常のパブリックAPIを使用してLifetimeWatcherのインスタンスと対話できます。しかし今、「残差」に関係する必要があるいくつかのテストは、いくつかのフープをジャンプすることによってその実装の詳細を取得することができます。

質問

これは正しいことですか?

これを行うと、頭の中でたくさんのアラームが鳴っています。

  • 実装の詳細に依存するテストは厳格です
  • 私は単体テストを作成していません。テストでは、この実装の詳細をGC.Collect()への呼び出しと組み合わせて使用​​しています
  • テスト中のソフトウェアは、テストのためだけに追加の作業を行っています。私のLifetimeWatcherクラスには、大声で叫ぶために「テスト」と呼ばれるものが含まれています
  • テストプロジェクトにのみ存在する特別なクラス(_LifetimeWatcher.ITestClient_を実装)を作成する必要があります

しかし同時に:

  • このクラスはいくつかの複雑なことを行っており、very変更に敏感です。私はこのクラスを重要な方法で破ることが難しいことを望んでおり、「残さ」を残すことは防ぐための重要なことです
  • 「残余」レベルは、実装の詳細です。したがって、テストしたい場合は、実装の詳細をテストするしかありません。少なくとも、テストでThread.Sleep(...)を呼び出したり、HTTPリクエストを行ったりしていない
  • インターフェイスを実装する必要があり、Acceptメソッドに渡したときにそのメンバーが入力されることさえ保証されていないため、他のユーザーが実装の詳細に依存することが確実に困難になります。
  • 将来のテストで追加の詳細な知識が必要な場合は、インターフェースにセッターを追加するのは簡単です
  • Acceptメソッドをインターフェイスの明示的な実装にして、ITestClientインターフェイスを別のファイルに移動することで、APIフットプリントをさらに削減できます。

Better Way™とは?

6
Matt Thomas

単体テストは内部状態を気にしません

私は、内部状態についての深い知識なしにテストするのが難しい動作を実装するクラスを作成しました。

これは矛盾です。テストについて話すとき、動作は具体的にpublic動作として定義されます。つまり、外部から見えるものです。内部状態は正反対です。
これは質問を無効にするわけではありませんが、期待と質問の両方を再構成します。

あなたの公共の行動が内部状態を使用してのみ観察できる場合、内部状態であるものは公共の行動の一部であるべきです。

簡単な例として、簡単な追加ツールを作成するとします。

_public class Adder
{
    public int Add(int a, int b)
    {
        // magic
    }
}
_

ポイントを証明するためにmagicと言います。このクラスのパブリック動作のみを認識し、内部動作は認識しません。この加算器が機能することをどのようにテストしますか?簡単なテストは次のとおりです。

_public void TestAdder()
{
    var adder = new Adder();

    Assert.AreEqual(adder.Add(1,1), 2);
    Assert.AreEqual(adder.Add(1,2), 3);
    Assert.AreEqual(adder.Add(2,1), 3);
}
_

これはおそらく動作テストですが、加算器が実際に内部でどのように動作するかはわかりませんでした。多分それは単純な_+_追加、多分それはライブラリを使用する、多分それはGoogleに尋ねる、多分それは数学フォーラムに質問を投稿して答えを解析する、多分それは多分私に答えを求めるために私にメールを送る。 。

単体テストの目的は、クラスの内部動作を完全に変更できるようにすると同時に、同じテストを実行して、変更によってクラスが壊れていないことを確認できるようにすることです。

テストが内部状態(またはその知識)に依存している場合は、(影響を受ける)内部構造が変化すると本質的に壊れるため、有用な単体テストにはなりません。


結果をテストする方法

_public class ComplicatedThing
{
    // The important behavior
    public void DoStuff() { ... }
}
_

DoStuff()を呼び出したときの永続的な影響(つまり、DoStuff()によって引き起こされた変更)は、次の2つのカテゴリのいずれかに当てはまります。

  • 内部状態
  • 次の2つのサブカテゴリのいずれかに該当する公共の行動:
    • 値を返す
    • 依存関係との相互作用

何をテストするつもりなのか、この効果がどこにあるのかを正確に示していないので、どちらの可能性にアプローチするかをリストします。

内部状態、例:

_public class ComplicatedThing
{
    private string _privateField = "joke";

    public void DoStuff()
    {
        _privateField = "secret";
    }
}
_

簡単に言えば、これはテストすべきではありません。このプライベートな値が「冗談」または「秘密」であるかどうかを知ることは、公共の行動の一部ではないため、単体テストでは問題になりません。

ただし、この内部状態が何らかの形で他の外部動作に影響を与える可能性があります(実際にそうである可能性があります)。次に例を示します。

_public class ComplicatedThing
{
    private string _privateField = "joke";

    public void DoStuff()
    {
        _privateField = "secret";
    }

    public string TellMeSomething()
    {
        return $"I know a {_privateField}";
    }
}
_

この公開動作(TellMeSomething()は、パブリック契約の一部であるため、テストすることができ、テストする必要があります。次のようなテストが期待されます。

_public void Tells_me_it_knows_a_joke_without_doing_stuff()
{
    // Arrange
    var thing = new ComplicatedThing();

    // Act
    var result = thing.TellMeSomething();

    // Assert
    Assert.AreEqual(result, "I know a joke");
}

public void Tells_me_it_knows_a_secret_after_doing_stuff()
{
    // Arrange
    var thing = new ComplicatedThing();

    // Act
    thing.DoStuff();
    var result = thing.TellMeSomething();

    // Assert
    Assert.AreEqual(result, "I know a secret");
}
_

しかし、何かをテストしたいからといって、外部の振る舞いを拡張すべきではないことをここで強調する必要があります。ビヘイビアの存在の正当化は、それをテストできるようにするだけでなく、コードベースの目的のためにそれを必要とすることであるべきです。

公共の行動であることをテストします。テストできるように、一般の行動を記述しないでください。

値を返す

先ほどお見せしたTellMeSomething()のテストは、すでに実際に答えています。あなたは単に返された結果を観察し、それがあなたの期待に適合していると主張します。

依存関係との相互作用、例:

_public class ComplicatedThing
{
    private Foo _foo;

    public ComplicatedThing(Foo foo)
    {
        _foo = foo;
    }

    public void DoStuff()
    {
        _foo.PostMessage("secret");
    }
}
_

これは、コンストラクターに注入される依存関係ですが、メソッドパラメーターとして渡される依存関係を持つこともできます。

_public class ComplicatedThing
{
    public void DoStuff(Foo foo)
    {
        foo.PostMessage("secret");
    }
}
_

現在のトピックでは、これらは同様に有効な依存関係です。残りの答えはどちらの場合にも当てはまります。

単体テストでは、すべての依存関係をモックする必要があります。これの良い点は、ユニットテストでモックオブジェクトを作成して、発生したことを密かに記録できるため、特定のメソッドが特定のパラメーターで呼び出されたかどうかを後で確認できることです。

私はこれにNSubstituteを使用する傾向がありますが、他のモックフレームワークが存在し、これを自分で行うこともできますが、さらに時間がかかります。

これを(NSubstitute構文を使用して)次のようにテストします。

_public void Posts_secret_message_to_foo()
{
    // Arrange
    var mockedFoo = Substitute.For<Foo>();        // this is a FAKE foo!
    var thing = new ComplicatedThing(mockedFoo);  // we make thing use the fake foo

    // Act
    thing.DoStuff();                              // thing doesn't know it's using a fake foo

    // Assert                                     // our fake foo secretly recorded how it was being handled by thing
    mockedFoo.Received(1).PostMessage("secret");
}
_

アサートは2つのことをテストします。

  • PostMessageが1回だけ呼び出されたことを保証します
  • PostMessageに渡されたメソッドパラメータが_"secret"_と等しいことを保証します

もちろん、これらのアサートは実際にアサートする必要があるものに変更できます。


結論

この答えには、特定のメソッドを呼び出すことによってもたらされる可能性のあるすべての影響と、それらのそれぞれをテストする方法が含まれています。

単体テストで内部状態を気にする必要がないという知識と組み合わせると(これは、単体テストが達成しようとしていることの概念に違反するため)、すべての可能性を排除しました。

提案するアプローチは正しくありません。間違い/誤解がどこにあるかに応じて、与えられた解決策の1つは適切なものです。要約する:

  • これは確かに内部状態であるべきです=>テストしないでください。
  • これは戻り値であるべきだった=>戻り値をテストする
  • これにより、依存関係で変更が発生するはずです=>依存関係を模擬し、期待どおりに相互作用したことを表明します
9
Flater

テストアセンブリを引用して、テスト中のクラスを InternalsVisibleToAttribute で修飾します。テストはクラスのinternalメンバーにアクセスでき、無関係な詳細でAPIを汚染する必要はありません。

5
Robert Harvey

これが正しい方法であるなら、私は何よりもまず出発点として賭けたいと思います。

おそらく「ユニット」(読み取り、関数)に直接リンクされていないフローをユニットテストするように「自分自身を強制」しようとしているため、最初からユニットテストを行うべきではないように感じます。

それが私であれば、たとえばコードの断片をより簡単にスタブできるように、または周囲のコードの正確さを保証することによって、あらゆるレベルでそれが正しいことを何とかして保証できるように、設計を再構築する方法を見つけます。のように:私がこれを単体テストして合格した場合、そして他のことも合格した場合、確かにこの内部ロジックが機能することを意味します。代理による保証の一種。

また、これがどのように当てはまるかは正確にはわかりません。しかし、テストで何かをサポートするために本番コードを変更する必要がある場合、私にはにおいのように感じます。

2
Bruno Oliveira

可能であれば、ComplicatedThingをより小さいThingに分割して、テストのためにこれらの状態を公開します。

1