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

17

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 AllFromCachequi remplirait le cache et retournerait s'il n'est pas là et reviendrait simplement de lui 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).

NikolaiDante
la source

Réponses:

18

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.

tdammers
la source
+1 c'est indéniablement la meilleure approche. Test unitaire pour vérifier la logique puis test d'intégration pour vérifier que le cache fonctionne comme prévu.
Tom Squires
10

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.

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

À l'aide d'outils comme PostSharp , vous pouvez le configurer de sorte 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, c'est 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.

pdr
la source
Que faites-vous si vous utilisez CachedProductRepository dans ProductManager mais souhaitez utiliser des méthodes qui se trouvent dans SQLProductRepository?
Jonathan
@Jonathan: "Il suffit d'avoir un référentiel et un wrapper de mise en cache qui utilisent la même interface" - s'ils ont la même interface, vous pouvez utiliser les mêmes méthodes. Le code appelant n'a besoin de rien savoir de l'implémentation.
pdr
3

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 nullou si aucun objet n'a la propriété demandée; ceux-ci peuvent également être testés.

En outre, vous pouvez considérer 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. Ceci est controversé, car certaines personnes (moi y compris) diraient que vous vous souciez de ce que vous récupérez de votre interface, pas de la façon dont vous l'obtenez (c'est-à-dire de vos tests que l'interface fonctionne comme prévu, pas qu'elle a une implémentation spécifique). Si vous le jugez important, vous avez la possibilité de le tester.


la source
1

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.

BЈовић
la source
0

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.

Daniel Hollinrake
la source
0

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 AllFromCache()méthode se charge 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.

jmruc
la source