web-dev-qa-db-ja.com

キャッシュを多用する単体テスト方法のベストプラクティス

キャッシュからオブジェクトとオブジェクトのリストを(フィルター処理して)保存および取得する多くのビジネスロジックメソッドがあります。

検討する

IList<TObject> AllFromCache() { ... }

TObject FetchById(guid id) { ... }

IList<TObject> FilterByPropertry(int property) { ... }

Fetch..およびFilter..AllFromCacheを呼び出します。これにより、キャッシュにデータが読み込まれ、キャッシュが存在しない場合は戻り、存在する場合はキャッシュから戻ります。

私は通常、これらのユニットテストを避けます。このタイプの構造に対するユニットテストのベストプラクティスは何ですか?

私はTestInitializeにキャッシュを設定し、TestCleanupでキャッシュを削除することを検討しましたが、それは私には正しくありません(そうなる可能性があります)。

18
NikolaiDante

真の単体テストが必要な場合は、キャッシュをモックする必要があります。キャッシュと同じインターフェイスを実装するモックオブジェクトを作成しますが、キャッシュではなく、受け取った呼び出しを追跡し、常に実際のものを返します。キャッシュはテストケースに従って返されます。

もちろん、キャッシュ自体にも単体テストが必要です。そのためには、依存するものをすべて模倣する必要があります。

実際のキャッシュオブジェクトを使用して、それを既知の状態に初期化し、テスト後にクリーンアップするということは、複数のユニットを同時にテストするため、統合テストに似ています。

19
tdammers

単一の責任の原則 は、ここであなたの親友です

まず、AllFromCache()をリポジトリクラスに移動し、GetAll()という名前を付けます。キャッシュから取得するのはリポジトリの実装の詳細であり、呼び出し元のコードからは認識されません。

これにより、フィルタリングクラスを簡単にテストできます。どこから取得するかを気にしません。

次に、データベース(またはどこでも)からデータを取得するクラスをキャッシングラッパーでラップします。

[〜#〜] aop [〜#〜] はこれに適した手法です。それは非常に得意な数少ないものの1つです。

PostSharp などのツールを使用して、選択した属性でマークされたメソッドがキャッシュされるように設定できます。ただし、これがキャッシュしている唯一のものである場合は、AOPフレームワークを用意する必要はありません。同じインターフェースを使用し、それを呼び出しクラスに挿入するリポジトリとキャッシングラッパーを用意するだけです。

例えば。

public class ProductManager
{
    private IProductRepository ProductRepository { get; set; }

    public ProductManager
    {
        ProductRepository = productRepository;
    }

    Product FetchById(guid id) { ... }

    IList<Product> FilterByPropertry(int property) { ... }
}

public interface IProductRepository
{
    IList<Product> GetAll();
}

public class SqlProductRepository : IProductRepository
{
    public IList<Product> GetAll()
    {
        // DB Connection, fetch
    }
}

public class CachedProductRepository : IProductRepository
{
    private IProductRepository ProductRepository { get; set; }

    public CachedProductRepository (IProductRepository productRepository)
    {
        ProductRepository = productRepository;
    }

    public IList<Product> GetAll()
    {
        // Check cache, if exists then return, 
        // if not then call GetAll() on inner repository
    }
}

ProductManagerからリポジトリ実装の知識を削除した方法を確認しますか?データの抽出を処理するクラス、データの取得を処理するクラス、およびキャッシュを処理するクラスを用意することで、単一責任の原則をどのように順守しているかも参照してください。

これで、これらのリポジトリのいずれかを使用してProductManagerをインスタンス化し、キャッシュを取得できます...これは、キャッシュの結果であると思われる紛らわしいバグが発生したときに、非常に役立ちます。

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

productManager = new ProductManager(
                         new CachedProductRepository(new SqlProductRepository())
                         );

(IOCコンテナーを使用している場合はさらに良いです。適応する方法は明らかです。)

そして、あなたのProductManagerテストで

IProductRepository repo = MockRepository.GenerateStrictMock<IProductRepository>();

キャッシュをテストする必要はまったくありません。

質問は次のようになります:そのCachedProductRepositoryをテストする必要がありますか?しないことをお勧めします。キャッシュはかなり不確定です。フレームワークは、あなたの制御の及ばないことを行います。たとえば、いっぱいになりすぎた場合は、その中身を取り除くだけです。あなたはブルームーンで一度失敗するテストで終わることになり、あなたは本当にその理由を本当に理解することは決してないでしょう。

そして、上で提案した変更を加えたので、テストするロジックはそれほど多くありません。本当に重要なテストであるフィルタリングメソッドが存在し、GetAll()の詳細から完全に抽象化されます。 GetAll()は...すべてを取得します。どこかから。

11
pdr

あなたの提案するアプローチが私がやろうとしていることです。説明のとおり、メソッドの結果は、オブジェクトがキャッシュに存在するかどうかに関係なく同じであるはずです。それでも同じ結果が得られるはずです。これは、各テストの前に特定の方法でキャッシュを設定することで簡単にテストできます。おそらく、GUIDがnullである場合や、要求されたプロパティを持つオブジェクトがない場合など、さらにいくつかのケースがあります。それらもテストできます。

さらに、mayは、オブジェクトが最初にキャッシュ内にあったかどうかに関係なく、メソッドが戻った後、オブジェクトがキャッシュ内に存在すると予想されることを考慮してください。 whatではなく、インターフェイスから戻ってくるhowについて気になると主張する人もいます(つまり、インターフェイスは期待どおりに機能しますが、特定の実装があるわけではありません)。あなたがそれを重要だと考えるなら、あなたはそれをテストする機会があります。

3
user4051

TestInitializeにキャッシュを設定し、TestCleanupでキャッシュを削除することを検討しましたが、それは適切ではありません

実際、それが唯一の正しい方法です。これが、これらの2つの関数の目的です。前提条件を設定し、クリーンアップします。前提条件が満たされていない場合、プログラムが機能しない可能性があります。

1
BЈовић

最近、キャッシュを使用するいくつかのテストに取り組んでいました。キャッシュで動作するクラスの周りにラッパーを作成し、このラッパーが呼び出されているというアサーションがありました。

これは主に、キャッシュで動作する既存のクラスが静的だったためです。

0

キャッシングロジックをテストしたいが、移入ロジックはテストしたくないようです。だから私はあなたがテストする必要がないものを模擬することをお勧めします-人口。

AllFromCache()メソッドは、キャッシュへの入力を処理します。これは、値の供給元など、他の何かに委任する必要があります。したがって、コードは次のようになります

private Supplier<TObject> supplier;

IList<TObject> AllFromCache() {
    if (!cacheInitialized) {
        //whatever logic needed to fill the cache
        cache.putAll(supplier.getValues());
        cacheInitialized = true;
    }

    return  cache.getAll();
}

これで、テスト用にサプライヤをモックして、事前定義された値を返すことができます。そうすることで、オブジェクトをロードせずに、実際のフィルタリングとフェッチをテストできます。

0
jmruc