Najlepsze praktyki dotyczące metod testowania jednostkowego, które intensywnie wykorzystują pamięć podręczną?

17

Mam wiele metod logiki biznesowej, które przechowują i pobierają (z filtrowaniem) obiekty i listy obiektów z pamięci podręcznej.

Rozważać

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

TObject FetchById(guid id) { ... }

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

Fetch..i Filter..zadzwoniłby, AllFromCachektóry zapełni pamięć podręczną i zwróci, jeśli nie ma, i po prostu wróci z niej, jeśli tak jest.

Zasadniczo unikam testów jednostkowych. Jakie są najlepsze praktyki dotyczące testowania jednostkowego w przypadku tego typu konstrukcji?

Zastanawiałem się nad zapełnieniem pamięci podręcznej w TestInitialize i usunięciem w TestCleanup, ale to nie wydaje mi się właściwe (może tak być).

NikolaiDante
źródło

Odpowiedzi:

18

Jeśli chcesz prawdziwych testów jednostkowych, musisz wyśmiewać pamięć podręczną: napisz próbny obiekt, który implementuje ten sam interfejs co pamięć podręczna, ale zamiast być pamięcią podręczną, śledzi odbierane połączenia i zawsze zwraca rzeczywiste pamięć podręczna powinna zwracać się zgodnie z przypadkiem testowym.

Oczywiście sama pamięć podręczna również wymaga testów jednostkowych, w których musisz wyśmiewać wszystko, od czego to zależy, i tak dalej.

To, co opisujesz, używając rzeczywistego obiektu pamięci podręcznej, ale inicjując go do znanego stanu i czyszcząc po teście, przypomina bardziej test integracji, ponieważ testujesz kilka jednostek jednocześnie.

tdammers
źródło
+1 to zdecydowanie najlepsze podejście. Test jednostkowy w celu sprawdzenia logiki, a następnie test integracji w celu sprawdzenia, czy pamięć podręczna działa zgodnie z oczekiwaniami.
Tom Squires
10

Zasada pojedynczej odpowiedzialności jest tutaj twoim najlepszym przyjacielem.

Przede wszystkim przenieś AllFromCache () do klasy repozytorium i nazwij ją GetAll (). To, że pobiera z pamięci podręcznej, jest szczegółem implementacji repozytorium i nie powinno być znane kodowi wywołującemu.

To sprawia, że ​​testowanie klasy filtrowania jest przyjemne i łatwe. Nie zależy już na tym, skąd go bierzesz.

Po drugie, owiń klasę, która pobiera dane z bazy danych (lub gdziekolwiek) w opakowanie buforujące.

AOP jest do tego dobrą techniką. To jedna z niewielu rzeczy, w których jest bardzo dobra.

Korzystając z narzędzi takich jak PostSharp , możesz to ustawić tak, aby każda metoda oznaczona wybranym atrybutem była buforowana. Jeśli jednak to jedyna rzecz, którą buforujesz, nie musisz sięgać tak daleko, jak posiadanie frameworka AOP. Wystarczy mieć repozytorium i opakowanie buforujące, które używają tego samego interfejsu i wstrzyknąć to do klasy wywołującej.

na przykład.

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

Zobacz, jak usunąłeś wiedzę dotyczącą implementacji repozytorium z menedżera produktów ProductManager? Zobacz także, w jaki sposób przestrzegałeś zasady pojedynczej odpowiedzialności, mając klasę, która obsługuje ekstrakcję danych, klasę, która obsługuje odzyskiwanie danych oraz klasę, która obsługuje buforowanie?

Możesz teraz utworzyć instancję menedżera produktu ProductManager z jednym z tych repozytoriów i uzyskać buforowanie ... lub nie. Jest to niezwykle przydatne później, gdy pojawi się mylący błąd, który, jak podejrzewasz, jest wynikiem pamięci podręcznej.

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

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

(Jeśli używasz kontenera IOC, nawet lepiej. Powinno być oczywiste, jak się przystosować.)

I w testach ProductManager

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

W ogóle nie trzeba testować pamięci podręcznej.

Teraz pojawia się pytanie: czy powinienem przetestować to CachedProductRepository? Sugeruję nie. Pamięć podręczna jest dość nieokreślona. Framework robi z nim rzeczy, które są poza twoją kontrolą. Na przykład po prostu usuwam z niego rzeczy, gdy się zapełni, na przykład. Skończysz z testami, które zawiodą raz na niebieskim księżycu i nigdy tak naprawdę nie zrozumiesz, dlaczego.

Po wprowadzeniu zmian, które zasugerowałem powyżej, naprawdę nie ma zbyt wiele logiki do przetestowania. Naprawdę ważny test, metoda filtrowania, będzie tam i całkowicie oderwany od szczegółów GetAll (). GetAll () po prostu ... dostaje wszystko. Skądś.

pdr
źródło
Co robisz, jeśli używasz CachedProductRepository w ProductManager, ale chcesz używać metod, które są w SQLProductRepository?
Jonathan
@Jathanathan: „Wystarczy mieć repozytorium i opakowanie buforujące, które używają tego samego interfejsu” - jeśli mają ten sam interfejs, możesz użyć tych samych metod. Kod wywołujący nie musi nic wiedzieć o implementacji.
pdr
3

Twoje sugerowane podejście jest tym, co bym zrobił. Biorąc pod uwagę twój opis, wynik metody powinien być taki sam, niezależnie od tego, czy obiekt jest obecny w pamięci podręcznej, czy nie: nadal powinieneś uzyskać ten sam wynik. Łatwo to przetestować, konfigurując pamięć podręczną w określony sposób przed każdym testem. Prawdopodobnie istnieją dodatkowe przypadki, na przykład gdy guid jest nulllub żaden obiekt nie ma żądanej właściwości; te też można przetestować.

Dodatkowo, ty może uznać, że oczekuje się, że obiekt będzie obecny w pamięci podręcznej po powrocie metody, niezależnie od tego, czy w ogóle był w pamięci podręcznej. Jest to kontrowersyjne, ponieważ niektórzy ludzie (w tym ja) twierdzą, że zależy ci na tym , co otrzymasz z interfejsu, a nie na tym , jak go uzyskasz (tj. Na testowaniu, czy interfejs działa zgodnie z oczekiwaniami, a nie że ma określoną implementację). Jeśli uznasz to za ważne, masz okazję to przetestować.


źródło
1

Rozważałem zapełnienie pamięci podręcznej podczas TestInitialize i usunięcie w TestCleanup, ale to nie wydaje mi się właściwe

W rzeczywistości jest to jedyny właściwy sposób. Do tego służą te dwie funkcje: ustalenie warunków wstępnych i oczyszczenie. Jeśli warunki wstępne nie zostaną spełnione, Twój program może nie działać.

BЈовић
źródło
0

Pracowałem ostatnio nad niektórymi testami, które używają buforowania. Utworzyłem opakowanie wokół klasy, które działa z pamięcią podręczną, a następnie miałem zapewnienia, że ​​to opakowanie zostało wywołane.

Zrobiłem to głównie dlatego, że istniejąca klasa, która działa z pamięcią podręczną, była statyczna.

Daniel Hollinrake
źródło
0

Wygląda na to, że chcesz przetestować logikę buforowania, ale nie logikę zapełniającą. Proponuję więc kpić z tego, czego nie trzeba testować - wypełniania.

Twoja AllFromCache()metoda zajmuje się zapełnianiem pamięci podręcznej i należy ją przekazać innej osobie, na przykład dostawcy wartości. Tak wyglądałby twój kod

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

Teraz możesz wyśmiewać dostawcę do testu, aby zwrócić niektóre wstępnie zdefiniowane wartości. W ten sposób możesz przetestować faktyczne filtrowanie i pobieranie, a nie ładowanie obiektów.

jmruc
źródło