Le migliori pratiche per i metodi di test delle unità che utilizzano pesantemente la cache?

18

Ho un numero di metodi di business logic che memorizzano e recuperano (con filtri) oggetti e liste di oggetti dalla cache.

Si consideri

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

TObject FetchById(guid id) { ... }

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

Fetch.. e Filter.. chiamerebbero AllFromCache che riempirebbe la cache e restituirebbe se non è presente e restituirà semplicemente da esso se lo è.

In genere evito i test unitari. Quali sono le migliori pratiche per il test unitario contro questo tipo di struttura?

Ho preso in considerazione l'inserimento della cache su TestInitialize e la rimozione su TestCleanup, ma non mi sembra giusto (anche se potrebbe essere).

    
posta NikolaiDante 29.01.2013 - 11:04
fonte

6 risposte

17

Se vuoi veri Test di unità, devi prendere in giro la cache: scrivi un oggetto fittizio che implementa la stessa interfaccia della cache, ma invece di essere una cache, tiene traccia delle chiamate che riceve e restituisce sempre cosa dovrebbe restituire la cache reale in base al test case.

Ovviamente anche la cache stessa ha bisogno di test delle unità, per cui devi prendere in giro qualsiasi cosa dipenda e così via.

Ciò che descrivi, utilizzando l'oggetto cache reale ma inizializzandolo a uno stato noto e pulendo dopo il test, è più simile a un test di integrazione, perché stai testando diverse unità in concerto.

    
risposta data 29.01.2013 - 12:37
fonte
10

Il principio di responsabilità singola è il tuo migliore amico qui.

Prima di tutto sposta AllFromCache () in una classe repository e chiamala GetAll (). Che recuperi dalla cache è un dettaglio di implementazione del repository e non dovrebbe essere conosciuto dal codice chiamante.

Questo rende la verifica della classe di filtro semplice e facile. Non ti interessa più da dove lo stai ricevendo.

In secondo luogo, avvolgere la classe che ottiene i dati dal database (o da qualsiasi luogo) in un wrapper di memorizzazione nella cache.

AOP è una buona tecnica per questo. È una delle poche cose a cui è molto bravo.

Utilizzando strumenti come PostSharp , puoi configurarlo in modo che qualsiasi metodo contrassegnato da l'attributo scelto verrà memorizzato nella cache. Tuttavia, se questa è l'unica cosa che si sta memorizzando nella cache, non è necessario arrivare ad avere un framework AOP. Basta avere un repository e un Caching Wrapper che utilizzino la stessa interfaccia e inseriscili nella classe chiamante.

ad es.

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

Scopri come hai rimosso le conoscenze sull'implementazione del repository da ProductManager? Guarda anche come hai aderito al Principio di Responsabilità Unica avendo una classe che gestisce l'estrazione dei dati, una classe che gestisce il recupero dei dati e una classe che gestisce la cache?

Ora è possibile creare un'istanza di ProductManager con uno di questi repository e ottenere il caching ... o meno. Questo è incredibilmente utile in seguito quando si ottiene un bug confuso che si sospetta sia un risultato della cache.

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

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

(Se stai usando un contenitore IOC, ancora meglio. Dovrebbe essere ovvio come adattarsi.)

E, nei tuoi test ProductManager

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

Non è necessario testare la cache.

Ora la domanda diventa: dovrei provare quel CachedProductRepository? Suggerisco di no Il cache è abbastanza indeterminato. Il framework fa cose che sono fuori dal tuo controllo. Ad esempio, rimuovendo qualcosa da esso quando diventa troppo pieno, per esempio. Finirai con test che falliscono una volta in una luna blu e non capirai mai veramente perché.

E, avendo apportato le modifiche sopra suggerite, non c'è davvero molta logica da testare lì. Il test davvero importante, il metodo di filtraggio, sarà presente e completamente estratto dal dettaglio di GetAll (). GetAll () solo ... ottiene tutto. Da qualche parte.

    
risposta data 29.01.2013 - 12:49
fonte
3

Il tuo approccio suggerito è quello che farei. Data la tua descrizione, il risultato del metodo dovrebbe essere lo stesso sia che l'oggetto sia presente nella cache o meno: dovresti comunque ottenere lo stesso risultato. È facile da testare impostando la cache in un modo particolare prima di ogni test. Ci sono probabilmente alcuni casi aggiuntivi come se il guid fosse null o nessun oggetto avesse la proprietà richiesta; anche quelli possono essere testati.

Inoltre, potrebbe considerare che ci si aspetta che l'oggetto sia presente nella cache dopo il ritorno del metodo, indipendentemente dal fatto che si trovasse nella cache in primo luogo. Questo è controverso, poiché alcune persone (me compreso) sosterrebbero che ti interessa che torni dalla tua interfaccia, non come hai capito (cioè il tuo test che il l'interfaccia funziona come previsto, non che abbia un'implementazione specifica). Se lo consideri importante, hai la possibilità di provarlo.

    
risposta data 29.01.2013 - 11:12
fonte
1

I considered populating cache on TestInitialize and removing on TestCleanup but that doesn't feel right to me

In realtà, questo è l'unico modo corretto di fare. Ecco a cosa servono queste due funzioni: impostare le condizioni preliminari e ripulire. Se le condizioni preliminari non sono soddisfatte, il tuo programma potrebbe non funzionare.

    
risposta data 29.01.2013 - 11:34
fonte
0

Stavo lavorando su alcuni test che utilizzano il caching di recente. Ho creato un wrapper attorno alla classe che funziona con la cache e ho quindi affermato che questo wrapper è stato chiamato.

L'ho fatto principalmente perché la classe esistente che funziona con la cache era statica.

    
risposta data 29.01.2013 - 12:04
fonte
0

Sembra che tu voglia testare la logica di caching, ma non la logica di popolamento. Quindi ti suggerisco di prendere in giro ciò che non hai bisogno di testare - popolando.

Il tuo metodo AllFromCache() si occupa di compilare la cache e questo dovrebbe essere delegato a qualcos'altro, come un fornitore di valori. Quindi il tuo codice sarà simile a

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

Ora puoi prendere in giro il fornitore per il test, per restituire alcuni valori predefiniti. In questo modo, puoi testare il tuo reale filtraggio e recupero e non caricare oggetti.

    
risposta data 29.01.2013 - 12:43
fonte

Leggi altre domande sui tag