非契約パラメータとは、class Person(string name)
のような、インターフェイスやサービスの依存関係ではないパラメータを意味します。
私はWebページのスクレイピングアプリケーションを作成していますが、これまでのところ、間違った順序で作成しています(そのため、この質問が作成されました)。 (文字列形式から)HTMLドキュメントを解析し、その中のすべてのURLと画像を取得するクラスを作成しました。
そのクラスには次の説明があります。
public class PageParser
{
private readonly string _html;
public PageParser(string html) {
_html = html;
}
public IEnumerable<string> GetImages() {
/* not important */
}
public IEnumerable<string> GetLinks() {
/* not important */
}
}
このコードは、ユニットテスト済み、100%カバレッジなど、うまく機能します。問題は、このクラスのコンストラクターを気にせずに、このクラスを使用するコードのユニットテストをどのように作成するかです。 PageParser
の動作を処理するには、この新しいクラスのみが必要です。
この新機能の擬似コードは次のとおりです。
public Report CreateReport(string url)
{
var html = _webClient.DownloadString(url);
var parser = new PageParser(html);
var images = parser.GetImages();
var links = parser.GetLinks();
var relevantLinks = links.Where(l => l.Contains('something'));
return _reportBuilder.Create(images, relevantLinks);
}
私が抱えている問題は、CreateReport
メソッドが持つことができるすべてのさまざまなテストケースに対してエピックサイズのHTMLドキュメントを含む単体テストの束を持ちたくないということです。
私が思いついたオプションは次のとおりです。
1)すべてそのままにします
クラス構造はそのままにして、大きなセットアップ変数を使用したテストを実行します。
2)ファクトリからページパーサー(+インターフェイス)を公開します
PageParser
のインターフェースを作成し、対応するインターフェースを持つファクトリーを作成します。
interface IPageParserFactory
{
IPageParser Create(string html);
}
class PageParserFactory : IPageParserFactory
{
IPageParser Create(string html)
{
return new PageParser(html);
}
}
その場合、結果の擬似コードは次のようになります。
// ... download string
var pageParser = _pageParserFactory.Create(html);
// ... create report
)パラメータhtmlを各メソッドに移動し、インターフェイスを作成します(したがって、クラスには構造の依存関係がありません)
interface IPageParser
{
IEnumerable<string> GetImages(string html);
IEnumerable<string> GetLinks(string html);
}
4)完全に他の何か
この問題を解決する別のパターンがある場合、それを知りたいと思います。
一見すると、CreateReport
メソッドは、多くのことを行うため、単一責任の原則に違反しているように見えます。
DownloadString
を介してURLからHTMLを読み取りますPageParser
を作成し、その解析結果を取得しますメソッド名を考えると、そのメソッドにはその最後の責任しかないと予想されます。代わりにここで提供したのは、データの取得、解析、およびフィルタリングのロジックと高度に結合されたものです。
レポートを作成するという考えは、通常、すでに準備されたある種のデータに基づいています。では、すでに疑問に思われるようになりましたが、なぜメソッドがURLを入力として使用するのでしょうか。
提案された解決策1)から3)まで、noneはこれらの問題に実際に対処します。これらのいずれかを実装する場合でも、単体テスト用のほぼ完全なHTMLコンテンツを含むローカルファイルを作成する必要があります。
一般的に遅すぎるため、ユニットテストでファイルシステムにアクセスしてはならないと主張する人さえいることに注意してください。もちろん、これに反対する人もいるかもしれませんが、少なくとも少し罪悪感を感じるはずです。そうするのには非常に正当な理由がある場合にのみそうします。この場合はそうしません。 。
では、オプション4)を使いましょう。問題を別の見方をすると、実際にはある種のドキュメントデータを処理することがすべてであることがわかります。あなたのPageParser
はそのようなデータを作成する最初のオブジェクトなので、個々のIEnumerable
sではなく単一のPageData
オブジェクトを返すようにデータを変更するとどうなるでしょうか。 IEnumerable
sを提供します。
ただし、この行を続けると、レポートの生成には実際にはPageData
オブジェクトのみが必要です。 _reportBuilder.Create(images, relevantLinks)
は_reportBuilder.Create(pageData)
の呼び出しになり、CreateReport
はURLの代わりにPageData
を入力として受け取ります。実際、CreateReport
はその1つのCreate
行に短縮される場合があります。とにかく、他のすべてはその責任ではありませんでした。
これらのクラス(PageParser
、PageData
、レポートジェネレータークラス)のいずれかの外部で、独立したPageDataFilter
インターフェイスと実装クラスを定義できます。それらの1つは、CreateReport
にあるフィルターになります。インターフェースは、PageData ApplyFilter(PageData pageData)
のような単純な1メソッドのフィルタリングであることに注意してください。このようなフィルターを処理すると、個々のフィルターのテストが容易になり、フィルターを簡単に構成でき、他のすべてのフィルターから分離できるという利点があります。レポートジェネレータは、指定されたPageData
が何らかの方法でフィルタリングされたかどうかを気にする必要はありません。 (実装では、PageData
が不変であるか、またはインプレースのフィルターで変更できるかを決定する必要があることに注意してください。最初のオプションは数学的に適切で、推論が容易で、並列化などに適しています。一方、後者はメモリとCPUの点でより効率的です。)
この時点で、PageParser
、レポートジェネレーター、およびリンクフィルターを簡単にテストできます。さらに、メモリ内に必要なクラスの1つの単純なインスタンスを準備するだけで、すべてのテストを実行できます。 URLの読み込み、PageParser
テスト以外の複雑なHTMLの処理、ファイルシステムへのアクセス、テストで処理する必要のあるカップリングは不要です。
補遺:上記のテキストに欠けているのは、これらのコンポーネントがどのように相互に配線されているかです。 PageParser
を作成するPageData
があり、PageDataFilter
インスタンスを介してフィルタリングし、ReportCreator
を介してレポートを作成できるとします。まだ必要なのは、これらのクラスのインスタンス化とインストルメンテーションです。そのための適切な場所がどこにあるかは、アプリケーションの設計によって異なります。ただし、簡単にするために、Application
メソッドを持つmain
クラスを想定できます。その方法では、次のようにします。
PageParser
をインスタンス化しますPageDataFilter
オブジェクトをインスタンス化しますPageDataFilter
オブジェクトをPageData
が提供するPageParser
に適用しますReportCreator
をインスタンス化し、フィルタリングされたPageData
を与えます。繰り返しになりますが、これらが手動でインスタンス化されているか、依存性が注入されているか、または何を持っているかは、全体的な設計とアーキテクチャに依存します。ただし、重要な点は、クラスを小さくし、唯一の責任に限定してから、外部からインストルメント化することです。インストルメンテーションコードが大きくなり、多くのことを実行する場合は、上記のアイデアをそのコードにも再帰的に適用します。