私は、内部状態についての深い知識なしにテストするのが難しい動作を実装するクラスを作成しました。この内部状態のアクセサーでクラスのパブリック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
_を実装)を作成する必要がありますしかし同時に:
Thread.Sleep(...)
を呼び出したり、HTTPリクエストを行ったりしていないAccept
メソッドに渡したときにそのメンバーが入力されることさえ保証されていないため、他のユーザーが実装の詳細に依存することが確実に困難になります。Accept
メソッドをインターフェイスの明示的な実装にして、ITestClient
インターフェイスを別のファイルに移動することで、APIフットプリントをさらに削減できます。Better Way™とは?
私は、内部状態についての深い知識なしにテストするのが難しい動作を実装するクラスを作成しました。
これは矛盾です。テストについて話すとき、動作は具体的に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つのカテゴリのいずれかに当てはまります。
何をテストするつもりなのか、この効果がどこにあるのかを正確に示していないので、どちらの可能性にアプローチするかをリストします。
内部状態、例:
_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つは適切なものです。要約する:
テストアセンブリを引用して、テスト中のクラスを InternalsVisibleToAttribute で修飾します。テストはクラスのinternal
メンバーにアクセスでき、無関係な詳細でAPIを汚染する必要はありません。
これが正しい方法であるなら、私は何よりもまず出発点として賭けたいと思います。
おそらく「ユニット」(読み取り、関数)に直接リンクされていないフローをユニットテストするように「自分自身を強制」しようとしているため、最初からユニットテストを行うべきではないように感じます。
それが私であれば、たとえばコードの断片をより簡単にスタブできるように、または周囲のコードの正確さを保証することによって、あらゆるレベルでそれが正しいことを何とかして保証できるように、設計を再構築する方法を見つけます。のように:私がこれを単体テストして合格した場合、そして他のことも合格した場合、確かにこの内部ロジックが機能することを意味します。代理による保証の一種。
また、これがどのように当てはまるかは正確にはわかりません。しかし、テストで何かをサポートするために本番コードを変更する必要がある場合、私にはにおいのように感じます。
可能であれば、ComplicatedThing
をより小さいThing
に分割して、テストのためにこれらの状態を公開します。