単体テストの利点に関するプレゼンテーションをまとめていますが、意図しない結果の簡単な例を示したいと思います: あるクラスのコードを変更すると、別のクラスの機能が損なわれます。
誰かがこれの例を簡単に説明することを提案できますか?
私の計画は、この機能に関する単体テストを作成して、すぐにテストを実行することで何かが壊れていることがわかっていることを実証することです。
単体テストの利点に関するプレゼンテーションをまとめていますが、意図しない結果の簡単な例を示したいと思います: あるクラスのコードを変更すると、別のクラスの機能が損なわれます。
誰かがこれの例を簡単に説明することを提案できますか?
私の計画は、この機能に関する単体テストを作成して、すぐにテストを実行することで何かが壊れていることがわかっていることを実証することです。
少し単純で、おそらくより明確な例は次のとおりです。
public string GetServerAddress()
{
return "172.0.0.1";
}
public void DoSomethingWithServer()
{
Console.WriteLine("Server address is: " + GetServerAddress());
}
GetServerAddress
配列を返すように変更した場合:
public string[] GetServerAddress()
{
return new string[] { "127.0.0.1", "localhost" };
}
DoSomethingWithServer からの出力は多少異なりますが、それでもすべてコンパイルされるため、さらに微妙なバグが発生します。
最初の (非配列) バージョンは出力さServer address is: 127.0.0.1
れ、2 番目のバージョンは出力されますServer address is: System.String[]
。これは、プロダクション コードでも見られたものです。言うまでもなく、もうありません!
次に例を示します。
class DataProvider {
public static IEnumerable<Something> GetData() {
return new Something[] { ... };
}
}
class Consumer {
void DoSomething() {
Something[] data = (Something[])DataProvider.GetData();
}
}
GetData()
を返すように変更するList<Something>
と、Consumer
壊れます。
これはやや不自然に見えるかもしれませんが、実際のコードで同様の問題を見てきました。
次のような方法があるとします。
abstract class ProviderBase<T>
{
public IEnumerable<T> Results
{
get
{
List<T> list = new List<T>();
using(IDataReader rdr = GetReader())
while(rdr.Read())
list.Add(Build(rdr));
return list;
}
}
protected abstract IDataReader GetReader();
protected T Build(IDataReader rdr);
}
さまざまな実装が使用されています。それらの1つはで使用されます:
public bool CheckNames(NameProvider source)
{
IEnumerable<string> names = source.Results;
switch(names.Count())
{
case 0:
return true;//obviously none invalid.
case 1:
//having one name to check is a common case and for some reason
//allows us some optimal approach compared to checking many.
return FastCheck(names.Single());
default:
return NormalCheck(names)
}
}
さて、これは特に奇妙なことではありません。IEnumerableの特定の実装を想定していません。確かに、これは配列と非常に多くの一般的に使用されるコレクションで機能します(System.Collections.Genericで、頭のてっぺんから一致しないものは考えられません)。通常のメソッドと通常の拡張メソッドのみを使用しました。単一アイテムのコレクション用に最適化されたケースがあることも珍しくありません。たとえば、リストを配列に変更したり、HashSet(重複を自動的に削除するため)、LinkedList、またはその他のいくつかのものに変更したりしても、機能し続けます。
それでも、特定の実装に依存しているわけではありませんが、特定の機能、具体的には巻き戻し可能機能に依存しています(Count()
ICollection.Countを呼び出すか、列挙可能なものを列挙してから、名前のチェックが行われます。
しかし、誰かがResultsプロパティを見て、「うーん、それは少し無駄だ」と思います。彼らはそれを次のものに置き換えます:
public IEnumerable<T> Results
{
get
{
using(IDataReader rdr = GetReader())
while(rdr.Read())
yield return Build(rdr);
}
}
これも完全に合理的であり、実際、多くの場合、パフォーマンスが大幅に向上します。問題のコーダーによって実行された即時の「テスト」でヒットしなかった場合CheckNames
(おそらく多くのコードパスでヒットしなかった場合)、CheckNamesがエラーになるという事実(そして、複数の名前。セキュリティ上のリスクが発生する場合は、さらに悪化する可能性があります)。
ただし、結果がゼロを超えるCheckNamesにヒットする単体テストは、それをキャッチします。
ちなみに、同等の(より複雑な場合)変更は、NPGSQLの下位互換性機能の理由です。List.Add()を戻り値に置き換えるほど単純ではありませんが、ExecuteReaderの動作方法を変更すると、最初の結果を得るためにO(n)からO(1)に同等の変更が加えられました。ただし、それ以前は、NpgsqlConnectionを使用すると、最初のリーダーがまだ開いている間と、接続が開いていないときに、ユーザーは接続から別のリーダーを取得できました。IDbConnectionのドキュメントには、これを行うべきではないと書かれていますが、実行中のコードがないという意味ではありません。幸いなことに、そのような実行中のコードの1つはNUnitテストであり、下位互換性機能が追加されて、構成を変更するだけでそのようなコードが機能し続けることができるようになりました。