Best Practices für Unit-Test-Methoden, die den Cache stark nutzen?

17

Ich habe eine Reihe von Geschäftslogikmethoden, die Objekte und Objektlisten aus dem Cache speichern und abrufen (mit Filterung).

Erwägen

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

TObject FetchById(guid id) { ... }

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

Fetch..und Filter..würde aufrufen, AllFromCachewas den Cache füllen und zurückgeben würde, wenn es nicht da ist, und einfach daraus zurückkehren, wenn es ist.

Ich scheue mich im Allgemeinen davor zurück, diese Geräte zu testen. Was sind die Best Practices für Unit-Tests für diese Art von Struktur?

Ich dachte darüber nach, den Cache bei TestInitialize zu füllen und bei TestCleanup zu entfernen, aber das fühlt sich für mich nicht richtig an (obwohl es gut sein könnte).

NikolaiDante
quelle

Antworten:

18

Wenn Sie echte Unit-Tests wünschen, müssen Sie den Cache verspotten: Schreiben Sie ein verspottetes Objekt, das die gleiche Schnittstelle wie der Cache implementiert. Statt jedoch ein Cache zu sein, werden die empfangenen Aufrufe verfolgt und immer die tatsächlichen zurückgegeben Der Cache sollte gemäß dem Testfall zurückgegeben werden.

Natürlich muss der Cache selbst auch Unit-Tests unterzogen werden, für die Sie alles verspotten müssen, wovon es abhängt, und so weiter.

Was Sie beschreiben, indem Sie das reale Cache-Objekt verwenden, es jedoch auf einen bekannten Status initialisieren und nach dem Test bereinigen, ähnelt eher einem Integrationstest, da Sie mehrere Einheiten gleichzeitig testen.

tdammers
quelle
+1 das ist trotzig der beste Ansatz. Komponententest zur Überprüfung der Logik und Integrationstest zur Überprüfung, ob der Cache erwartungsgemäß funktioniert.
Tom Squires
10

Das Prinzip der Einzelverantwortung ist hier Ihr bester Freund.

Verschieben Sie zunächst AllFromCache () in eine Repository-Klasse und nennen Sie sie GetAll (). Das Abrufen aus dem Cache ist ein Implementierungsdetail des Repositorys und sollte dem aufrufenden Code nicht bekannt sein.

Dies macht das Testen Ihrer Filterklasse angenehm und einfach. Es ist nicht länger wichtig, woher du es bekommst.

Binden Sie anschließend die Klasse, die die Daten aus der Datenbank (oder von einem beliebigen Ort) abruft, in einen Caching-Wrapper ein.

AOP ist dafür eine gute Technik. Es ist eines der wenigen Dinge, in denen es sehr gut ist.

Mit Tools wie PostSharp können Sie festlegen, dass alle mit einem ausgewählten Attribut gekennzeichneten Methoden zwischengespeichert werden. Wenn dies jedoch das einzige ist, das Sie zwischenspeichern, müssen Sie nicht so weit gehen, dass Sie über ein AOP-Framework verfügen. Stellen Sie einfach ein Repository und einen Caching-Wrapper bereit, die dieselbe Schnittstelle verwenden, und fügen Sie diese in die aufrufende Klasse ein.

z.B.

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

Sehen Sie, wie Sie das Repository-Implementierungswissen aus dem ProductManager entfernt haben? Sehen Sie auch, wie Sie das Prinzip der Einzelverantwortung eingehalten haben, indem Sie eine Klasse für die Datenextraktion, eine Klasse für den Datenabruf und eine Klasse für die Zwischenspeicherung eingerichtet haben?

Sie können jetzt den ProductManager mit einem dieser Repositorys instanziieren und Zwischenspeicherung durchführen ... oder nicht. Dies ist später unglaublich nützlich, wenn Sie einen verwirrenden Fehler bemerken, von dem Sie vermuten, dass er auf den Cache zurückzuführen ist.

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

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

(Wenn Sie einen IOC-Container verwenden, ist dies noch besser. Die Anpassung sollte offensichtlich sein.)

Und in Ihren ProductManager-Tests

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

Der Cache muss überhaupt nicht getestet werden.

Jetzt lautet die Frage: Soll ich das CachedProductRepository testen? Ich schlage nicht vor. Der Cache ist ziemlich unbestimmt. Das Framework erledigt damit Dinge, die außerhalb Ihrer Kontrolle liegen. Zum Beispiel einfach Sachen entfernen, wenn sie zu voll sind. Du wirst mit Tests enden, die einmal in einem blauen Mond scheitern und du wirst nie wirklich verstehen warum.

Und nachdem ich die oben vorgeschlagenen Änderungen vorgenommen habe, gibt es wirklich nicht so viel Logik zum Testen. Der wirklich wichtige Test, die Filtermethode, wird vorhanden und vollständig vom Detail von GetAll () abstrahiert sein. GetAll () bekommt einfach alles. Von irgendwo.

pdr
quelle
Was tun Sie, wenn Sie CachedProductRepository in ProductManager verwenden, aber Methoden verwenden möchten, die sich in SQLProductRepository befinden?
Jonathan
@Jonathan: "Nur ein Repository und ein Caching Wrapper, die dieselbe Schnittstelle verwenden" - wenn sie dieselbe Schnittstelle haben, können Sie dieselben Methoden verwenden. Der aufrufende Code muss nichts über die Implementierung wissen.
pdr
3

Ihr vorgeschlagener Ansatz ist das, was ich tun würde. In Anbetracht Ihrer Beschreibung sollte das Ergebnis der Methode das gleiche sein, unabhängig davon, ob sich das Objekt im Cache befindet oder nicht: Sie sollten immer noch das gleiche Ergebnis erhalten. Dies ist einfach zu testen, indem der Cache vor jedem Test auf eine bestimmte Weise eingerichtet wird. Es gibt wahrscheinlich einige zusätzliche Fälle, z. B. wenn die Guid nulldie angeforderte Eigenschaft aufweist oder kein Objekt vorhanden ist. diese können auch getestet werden.

Zusätzlich Sie können prüfen , zu erwarten , dass sich das Objekt im Cache nach der Methode zurückgibt , vorhanden sein, unabhängig davon , ob sie im Cache in erster Linie war. Dies ist umstritten, da einige Leute (ich selbst eingeschlossen) argumentieren würden, dass es Ihnen wichtig ist, was Sie von Ihrer Schnittstelle zurückerhalten, und nicht, wie Sie es erhalten (dh Sie testen, ob die Schnittstelle wie erwartet funktioniert und keine spezifische Implementierung hat). Wenn Sie es für wichtig halten, haben Sie die Möglichkeit, dies zu testen.


quelle
1

Ich habe darüber nachgedacht, den Cache bei TestInitialize zu füllen und bei TestCleanup zu entfernen, aber das scheint mir nicht richtig zu sein

Eigentlich ist das der einzig richtige Weg. Dafür gibt es diese beiden Funktionen: Voraussetzungen schaffen und aufräumen. Wenn die Voraussetzungen nicht erfüllt sind, funktioniert Ihr Programm möglicherweise nicht.

BЈовић
quelle
0

Ich habe in letzter Zeit an einigen Tests mit Cache gearbeitet. Ich habe einen Wrapper für die Klasse erstellt, der mit dem Cache funktioniert, und dann die Zusicherung erhalten, dass dieser Wrapper aufgerufen wird.

Ich tat dies hauptsächlich, weil die vorhandene Klasse, die mit Cache arbeitet, statisch war.

Daniel Hollinrake
quelle
0

Anscheinend möchten Sie die Caching-Logik testen, aber nicht die Auffüllungslogik. Daher würde ich vorschlagen, dass Sie sich über das lustig machen, was Sie nicht testen müssen - das Auffüllen.

Ihre AllFromCache()Methode kümmert sich um das Auffüllen des Caches, und dieser sollte an etwas anderes delegiert werden, beispielsweise an einen Wertelieferanten. So würde Ihr Code aussehen

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

Jetzt können Sie den Lieferanten für den Test verspotten, um einige vordefinierte Werte zurückzugeben. Auf diese Weise können Sie das tatsächliche Filtern und Abrufen testen und keine Objekte laden.

jmruc
quelle