web-dev-qa-db-fra.com

Meilleures pratiques pour les méthodes de test unitaire qui utilisent beaucoup le cache?

J'ai un certain nombre de méthodes de logique métier qui stockent et récupèrent (avec filtrage) les objets et les listes d'objets du cache.

Considérer

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

TObject FetchById(guid id) { ... }

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

Fetch.. et Filter.. appellerait AllFromCache qui remplirait le cache et retournerait s'il n'est pas là et en reviendrait simplement s'il l'est.

Je répugne généralement à tester ces unités. Quelles sont les meilleures pratiques pour les tests unitaires contre ce type de structure?

J'ai envisagé de remplir le cache sur TestInitialize et de le supprimer sur TestCleanup mais cela ne me semble pas correct (cela pourrait bien l'être).

18
NikolaiDante

Si vous voulez de vrais tests unitaires, alors vous devez vous moquer du cache: écrivez un objet simulé qui implémente la même interface que le cache, mais au lieu d'être un cache, il garde la trace des appels qu'il reçoit et renvoie toujours ce que le réel le cache doit être renvoyé conformément au cas de test.

Bien sûr, le cache lui-même a également besoin de tests unitaires, pour lesquels vous devez vous moquer de tout ce dont il dépend, etc.

Ce que vous décrivez, en utilisant le véritable objet cache mais en l'initialisant à un état connu et en le nettoyant après le test, ressemble plus à un test d'intégration, car vous testez plusieurs unités de concert.

19
tdammers

Le principe de responsabilité unique est votre meilleur ami ici.

Tout d'abord, déplacez AllFromCache () dans une classe de référentiel et appelez-la GetAll (). Qu'il récupère du cache est un détail d'implémentation du référentiel et ne devrait pas être connu par le code appelant.

Cela rend le test de votre classe de filtrage agréable et facile. Il ne se soucie plus d'où vous l'obtenez.

Ensuite, encapsulez la classe qui obtient les données de la base de données (ou n'importe où) dans un wrapper de mise en cache.

AOP est une bonne technique pour cela. C'est l'une des rares choses dans lesquelles il est très bon.

En utilisant des outils comme PostSharp , vous pouvez le configurer pour que toute méthode marquée avec un attribut choisi soit mise en cache. Cependant, si c'est la seule chose que vous mettez en cache, vous n'avez pas besoin d'aller aussi loin que d'avoir un framework AOP. Il suffit d'avoir un référentiel et un wrapper de mise en cache qui utilisent la même interface et l'injectent dans la classe appelante.

par exemple.

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
    }
}

Vous voyez comment vous avez supprimé les connaissances d'implémentation du référentiel du ProductManager? Voyez également comment vous avez adhéré au principe de responsabilité unique en ayant une classe qui gère l'extraction des données, une classe qui gère la récupération des données et une classe qui gère la mise en cache?

Vous pouvez maintenant instancier le ProductManager avec l'un de ces référentiels et obtenir la mise en cache ... ou non. Cela est incroyablement utile plus tard lorsque vous obtenez un bug déroutant que vous soupçonnez être le résultat du cache.

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

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

(Si vous utilisez un conteneur IOC, encore mieux. Il devrait être évident de savoir comment vous adapter.)

Et, dans vos tests ProductManager

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

Pas besoin de tester le cache du tout.

Maintenant, la question devient: Dois-je tester ce CachedProductRepository? Je suggère que non. Le cache est assez indéterminé. Le framework fait des choses hors de votre contrôle. Par exemple, en supprimant simplement des éléments lorsqu'ils sont trop pleins, par exemple. Vous allez vous retrouver avec des tests qui échouent une fois dans une lune bleue et vous ne comprendrez jamais vraiment pourquoi.

Et, après avoir apporté les modifications que j'ai suggérées ci-dessus, il n'y a vraiment pas beaucoup de logique à tester là-dedans. Le test vraiment important, la méthode de filtrage, sera là et complètement abstrait du détail de GetAll (). GetAll () juste ... obtient tout. De quelque part.

11
pdr

Votre approche suggérée est ce que je ferais. Compte tenu de votre description, le résultat de la méthode devrait être le même que l'objet soit présent dans le cache ou non: vous devriez toujours obtenir le même résultat. C'est facile à tester en configurant le cache d'une manière particulière avant chaque test. Il y a probablement quelques cas supplémentaires comme si le guid est null ou qu'aucun objet n'a la propriété demandée; ceux-ci peuvent également être testés.

De plus, vous pouvez considérez qu'il s'attendait à ce que l'objet soit présent dans le cache après le retour de votre méthode, qu'il soit dans le cache en premier lieu. C'est controversé, car certaines personnes (moi y compris) diraient que vous vous souciez de quoi vous revenez de votre interface, pas comment vous l'obtenez (c'est-à-dire vos tests que le l'interface fonctionne comme prévu, pas qu'elle ait une implémentation spécifique). Si vous le jugez important, vous avez la possibilité de le tester.

3
user4051

J'ai envisagé de remplir le cache sur TestInitialize et de le supprimer sur TestCleanup mais cela ne me semble pas correct

En fait, c'est la seule façon correcte de le faire. C'est à cela que servent ces deux fonctions: fixer les conditions préalables et nettoyer. Si les conditions préalables ne sont pas remplies, votre programme peut ne pas fonctionner.

1
BЈовић

Je travaillais sur certains tests qui utilisent le cache récemment. J'ai créé un wrapper autour de la classe qui fonctionne avec le cache, puis j'ai affirmé que ce wrapper était appelé.

Je l'ai fait principalement parce que la classe existante qui fonctionne avec le cache était statique.

0
Daniel Hollinrake

Il semble que vous souhaitiez tester la logique de mise en cache, mais pas la logique de remplissage. Je vous suggère donc de vous moquer de ce que vous n'avez pas besoin de tester - remplir.

Votre méthode AllFromCache() s'occupe de remplir le cache, et cela devrait être délégué à autre chose, comme un fournisseur de valeurs. Donc, votre code ressemblerait à

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

Vous pouvez maintenant vous moquer du fournisseur pour le test, pour renvoyer des valeurs prédéfinies. De cette façon, vous pouvez tester votre filtrage et votre récupération réels, et non le chargement d'objets.

0
jmruc