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


17

私は、キャッシュからオブジェクトとオブジェクトのリストを(フィルターを使用して)保存および取得するビジネスロジックメソッドをいくつか持っています。

検討する

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

TObject FetchById(guid id) { ... }

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

Fetch..そして、Filter..呼んでAllFromCache、それがない場合、キャッシュとリターンを移入し、ちょうどそれがある場合、そこから返していました。

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

TestInitializeでキャッシュにデータを追加し、TestCleanupでキャッシュを削除することを検討しましたが、それは私には適切ではないと感じました(そうかもしれません)。

回答:


18

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

もちろん、キャッシュ自体もユニットテストを必要とします。そのため、依存するものは何でもモックする必要があります。

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


+1これは間違いなく最良のアプローチです。単体テストでロジックをチェックし、次に統合テストで実際にキャッシュが期待どおりに機能することを確認します。
トムスクワイアズ

10

シングル責任原則はここにあなたの親友です。

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

これにより、フィルタリングクラスを簡単にテストできます。取得元を気にしなくなりました。

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

AOPはこれに適した手法です。それは非常に得意な数少ないものの一つです。

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()だけ...すべてを取得します。どこかから。


ProductManagerでCachedProductRepositoryを使用しているが、SQLProductRepositoryにあるメソッドを使用する場合はどうしますか?
ジョナサン14

@Jonathan:「同じインターフェイスを使用するリポジトリとキャッシングラッパーがあれば」-同じインターフェイスを使用している場合は、同じメソッドを使用できます。呼び出しコードは、実装について何も知る必要はありません。
pdr 14

3

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

また、オブジェクトが最初にキャッシュにあったかどうかに関係なく、メソッドが戻った後、オブジェクトがキャッシュに存在すると予想されると考えるかもしれませ。これは議論の余地があります。一部の人々(私自身も含む)は、インターフェイスの取得方法ではなく、インターフェイスから取得するものに関心があると主張します(つまり、特定の実装ではなく、インターフェイスが期待どおりに動作することをテストします)。あなたがそれを重要と考えるなら、あなたはそれをテストする機会があります。


1

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

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


0

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

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


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();
}

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.