言い換えれば、コードの詳細なアルゴリズムを正確に知っている必要があります。
完全ではありません。コード自体の外部から観察されるように、コードの詳細な動作を正確に知る必要があります。この動作を実現するアルゴリズム、またはアルゴリズムの組み合わせ、または任意のレベルの抽象化/ネスト/計算など。テストには重要ではありません。テストは、目的の結果が達成されることだけを考慮します。
したがって、テストの価値は、コードがどのように動作するかを指定することです。したがって、テストに対して検証できる限り、コードは必要なものをすべて自由に変更できます。パフォーマンスの向上、読みやすさとサポート性のリファクタリングなどを行うことができます。テストにより、動作が変更されないことが確認されます。
たとえば、2つの数値を加算する関数を作成するとします。頭の中でそれをどのように実装するかを知っているかもしれませんが、その知識はしばらく脇に置いておきます。まだ実装していません。まず、テストを実装しています...
public void CanAddIntegers()
{
var addend = 1;
var augend = 1;
var result = MyMathObject.Add(addend, augend);
Assert.AreEqual(2, result);
}
テストが完了したので、メソッドを実装できます...
public int Add(int addend, int augend)
{
return ((addend * 2) + (augend * 2)) / 2;
}
うわあ。ちょっと待ってください...なぜ私はそれをそのように実装したのですか?さて、テストの観点から、誰が気にしますか?合格です。実装は要件を満たしています。テストが完了したので、コードを安全にリファクタリングできます...
public int Add(int addend, int augend)
{
return addend + augend;
}
それはもう少し正気です。そして、テストはまだ合格です。実際、コードをさらに減らすことができます...
public int Add(int addend, int augend)
{
return 2;
}
何だと思う?テストはまだ合格です。これは私たちが持っている唯一のテストであり、与えられた唯一の仕様なので、コードは「機能します」。したがって、より多くのケースをカバーするために、明らかにテストを改善する必要があります。より多くのテストを作成すると、より多くのコードを作成するために必要な仕様が得られます。
実際、 TDDの3番目のルールによれば、その最後の実装は最初の実装である必要があります。
1つの失敗した単体テストに合格するのに十分な数を超える本番コードを作成することは許可されていません。
したがって、純粋にUncle-Bob主導のTDDの世界では、最初にその最後の実装を記述し、次にさらにテストを記述して、コードを徐々に改善していました。
これは、赤、緑、リファクタリングサイクルとして知られています。これは、少し工夫が凝らされていない単純な例であるボウリングゲームで非常によく示されています。その演習の目的は、そのサイクルを練習することです。
- まず、特定の動作を期待するテストを作成します。これはサイクルの赤い部分です。これは、適切な動作がないとテストが失敗するためです。
- 次に、その動作を示すコードを記述します。その目的はテストに合格することであるため、これはサイクルのグリーン部分です。そして、テストに合格するためだけに。
- 最後に、コードをリファクタリングして改善します。これは、当然、サイクルのリファクタリング部分です。
行き詰まっているのは、サイクルのリファクタリング部分に永続的にいるということです。あなたはすでにコードをより良くする方法について考えています。どのアルゴリズムが正しいか、それをどのように最適化するか、最終的にどのように書くべきか。そのために、TDDは忍耐力の練習です。最高のコードを書かないでください...まだ。
- まず、コードが何をすべきかを決定します。それ以上は何もしません。
- 次に、それを実行するコードを記述します。それ以上は何もしません。
- 最後に、そのコードを改善し、改善します。
アップデート
この質問を思い出させる何かに出くわしましたが、ランダムに思い浮かびました。おそらく私はあなたが求めていることの状況を誤解しました。依存関係をどのように管理していますか?つまり、どのような依存性注入手法を使用していますか?それがここで議論されている問題の根源かもしれないように思えます。
私が覚えている限り、私はCommon Service Locator(または、より一般的には、同じ概念の自家製の実装)のようなものを使用してきました。そしてそうすることで、私は依存性注入の非常に特殊なスタイルに向かう傾向があります。別のスタイルを使用しているようです。コンストラクター注入、おそらく?この答えのために、コンストラクターインジェクションを想定します。
MyMathObject
それで、あなたが示すように、それがとに依存しているMyOtherClass1
としましょうMyOtherClass2
。コンストラクタインジェクションを使用すると、のフットプリントは次のMyMathObject
ようになります。
public class MyMathObject
{
public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
{
// implementation details
}
public int Add(int addend, int augend)
{
// implementation details
}
}
したがって、ご指摘のとおり、テストでは依存関係またはそのモックを提供する必要があります。クラスのフットプリントには、またはの実際の使用の兆候はありませんが、それらの必要性の兆候はあります。依存関係として、それらはコンストラクターによって大声でアドバタイズされます。MyOtherClass1
MyOtherClass2
だから、これはあなたが尋ねた質問を懇願します...オブジェクトをまだ実装していないときに、最初にテストを書くにはどうすればよいですか?繰り返しになりますが、オブジェクトの外向きのデザインだけで実際の使用を示すものはありません。したがって、依存関係は、知っておく必要のある実装の詳細です。
それ以外の場合は、最初にこれを記述します。
public class MyMathObject
{
public int Add(int addend, int augend)
{
// implementation details
}
}
次に、テストを作成し、それを実装して依存関係を発見し、テストを再作成します。そこに問題があります。
ただし、あなたが見つけた問題は、テストやテスト駆動開発の問題ではありません。問題は実際にはオブジェクトのデザインにあります。釉薬がかけられているという事実にもかかわらず、// implementation details
まだ逃げている実装の詳細があります。漏れのある抽象化があります:
public class MyMathObject
{
public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
{ ^---Right here ^---And here
// implementation details
}
public int Add(int addend, int augend)
{
// implementation details
}
}
オブジェクトは、実装の詳細を十分にカプセル化および抽象化していない。それは試みており、依存性注入の使用はそれに向けた大きな一歩です。しかし、まだ完全にはありません。これは、実装の詳細である依存関係が外部から見え、他のオブジェクトによって外部から認識されているためです。(この場合、テストオブジェクトです。)したがって、依存関係を満たし、機能させるにはMyMathObject
、外部オブジェクトがその実装の詳細を知る必要があります。それらはすべてそうです。テストオブジェクト、それを使用するプロダクションコードオブジェクト、それに依存するあらゆるもの。
そのためには、依存関係の管理方法を切り替えることを検討することをお勧めします。コンストラクターインジェクションやセッターインジェクションのようなものの代わりに、依存関係の管理をさらに反転させ、オブジェクトにさらに別のオブジェクトを介してそれらを内部的に解決させます。
前述のサービスロケーターを開始パターンとして使用すると、依存関係を解決することが唯一の目的(単一責任)であるオブジェクトを作成するのは非常に簡単です。依存性注入フレームワークを使用している場合、このオブジェクトは通常、フレームワークの機能の単なるパススルーです(ただし、フレームワーク自体を抽象化するため、依存性が1つ少なくなります。これは良いことです)。自家製の機能を使用する場合、このオブジェクトはその機能を抽象化します。
しかし、最終的には次のようなものになりますMyMathObject
。
private SomeInternalFunction()
{
var firstDependency = ServiceLocatorObject.Resolve<MyOtherClass1>();
// implementation details
}
したがってMyMathObject
、依存性注入を使用した場合でも、のフットプリントは次のようになります。
public class MyMathObject
{
public int Add(int addend, int augend)
{
// implementation details
}
}
リークのある抽象化や外部的に知られている依存関係はありません。実装の詳細が変更されたため、テストを変更する必要はありません。これは、テスト対象のオブジェクトからテストを分離するためのもう1つのステップです。