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


17

Ho una serie di metodi di logica aziendale che memorizzano e recuperano (con filtro) oggetti ed elenchi di oggetti dalla cache.

Ritenere

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

TObject FetchById(guid id) { ... }

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

Fetch..e Filter..chiamerebbe AllFromCacheche popolerebbe la cache e ritornerebbe se non c'è e ritornerebbe da essa se lo è.

In genere mi rifuggo dall'unità di test di questi. Quali sono le migliori pratiche per i test unitari contro questo tipo di struttura?

Ho considerato di popolare la cache su TestInitialize e di rimuoverla su TestCleanup, ma ciò non mi sembra giusto (anche se potrebbe essere).

Risposte:


18

Se vuoi veri test di unità, devi prendere in giro la cache: scrivi un oggetto simulato che implementa la stessa interfaccia della cache, ma invece di essere una cache, tiene traccia delle chiamate che riceve e restituisce sempre ciò che il reale la cache dovrebbe tornare in base al caso di test.

Naturalmente la cache stessa ha anche bisogno di test unitari, per i quali devi deridere qualsiasi cosa da cui dipende, e così via.

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


+1 questo è provocatoriamente l'approccio migliore. Unit test per verificare la logica, quindi test di integrazione per verificare effettivamente che la cache funzioni come previsto.
Tom Squires,

10

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

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

Questo rende la prova della tua classe di filtraggio piacevole e facile. Non importa più da dove lo stai ricevendo.

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

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

Usando strumenti come PostSharp , puoi configurarlo in modo che qualsiasi metodo contrassegnato con un attributo scelto verrà memorizzato nella cache. Tuttavia, se questa è l'unica cosa che stai memorizzando nella cache, non hai bisogno di andare fino al punto di avere un framework AOP. Basta avere un repository e un wrapper per la memorizzazione nella cache che utilizzano la stessa interfaccia e lo inseriscono nella classe chiamante.

per esempio.

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

Vedi come hai rimosso la conoscenza dell'implementazione del repository dal ProductManager? Vedi anche come hai aderito al principio di responsabilità singola avendo una classe che gestisce l'estrazione dei dati, una classe che gestisce il recupero dei dati e una classe che gestisce la memorizzazione nella cache?

Ora puoi creare un'istanza del ProductManager con uno di questi repository e ottenere la memorizzazione nella cache ... oppure no. Questo è incredibilmente utile in seguito quando si ottiene un bug confuso che si sospetta sia il risultato della cache.

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

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

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

E, nei test di ProductManager

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

Non è necessario testare la cache.

Ora la domanda diventa: dovrei testare quel CachedProductRepository? Suggerisco di no. La cache è piuttosto indeterminata. Il framework fa cose che sono fuori dal tuo controllo. Ad esempio, rimuovendo le cose da esso quando diventa troppo pieno, per esempio. Finirai con prove che falliscono una volta in una luna blu e non capirai mai davvero perché.

E, dopo aver apportato le modifiche che ho suggerito sopra, non c'è davvero molta logica da testare. Il test veramente importante, il metodo di filtraggio, sarà lì e completamente astratto dai dettagli di GetAll (). GetAll () solo ... ottiene tutto. Da qualche parte.


Cosa fare se si utilizza CachedProductRepository in ProductManager ma si desidera utilizzare metodi che si trovano in SQLProductRepository?
Jonathan,

@Jonathan: "Basta avere un repository e un wrapper di memorizzazione nella cache che utilizzano la stessa interfaccia" - se hanno la stessa interfaccia, è possibile utilizzare gli stessi metodi. Il codice chiamante non deve sapere nulla sull'implementazione.
pdr

3

Il tuo approccio suggerito è quello che farei. Data la tua descrizione, il risultato del metodo dovrebbe essere lo stesso indipendentemente dal fatto 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. Probabilmente ci sono alcuni casi aggiuntivi come se il guid è nullo nessun oggetto ha la proprietà richiesta; anche quelli possono essere testati.

Inoltre, è possibile prevedere che l'oggetto sia presente nella cache dopo il ritorno del metodo, indipendentemente dal fatto che fosse nella cache in primo luogo. Questo è controverso, poiché alcune persone (me compreso) direbbero che ti importa di ciò che torni dalla tua interfaccia, non di come lo ottieni (cioè il test che l'interfaccia funziona come previsto, non che abbia un'implementazione specifica). Se lo consideri importante, hai l'opportunità di provarlo.


1

Ho considerato di popolare la cache su TestInitialize e di rimuoverla su TestCleanup, ma non mi sembra giusto

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


0

Stavo lavorando su alcuni test che utilizzano la cache di recente. Ho creato un wrapper per la classe che funziona con la cache e quindi ho affermato che questo wrapper veniva chiamato.

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


0

Sembra che tu voglia testare la logica di memorizzazione nella cache, ma non la logica di popolamento. Quindi suggerirei di prendere in giro ciò che non è necessario testare - la popolazione.

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

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 deridere il fornitore per il test, per restituire alcuni valori predefiniti. In questo modo, puoi testare il filtro e il recupero effettivi e non caricare oggetti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.