Jak utworzyć skalowalne i wolne od skutków ubocznych testy integracyjne?

14

W moim obecnym projekcie trudno mi znaleźć dobre rozwiązanie do tworzenia skalowalnych testów integracyjnych, które nie mają skutków ubocznych. Trochę wyjaśnienia na temat właściwości wolnych od skutków ubocznych: chodzi głównie o bazę danych; po zakończeniu testów nie powinno być żadnych zmian w bazie danych (należy zachować stan). Może skalowalność i zachowanie stanu nie idą w parze, ale naprawdę chcę naciskać na lepsze rozwiązanie.

Oto typowy test integracji (testy dotykają warstwy bazy danych):

public class OrderTests {

    List<Order> ordersToDelete = new ArrayList<Order>(); 

    public testOrderCreation() {
        Order order = new Order();
        assertTrue(order.save());
        orderToDelete.add(order);
    }

    public testOrderComparison() {
        Order order = new Order();
        Order order2 = new Order();
        assertFalse(order.isEqual(order2);
        orderToDelete.add(order);
        orderToDelete.add(order2);
    }
    // More tests

    public teardown() {
         for(Order order : ordersToDelete)
             order.delete();
    }
}

Jak można sobie wyobrazić, takie podejście daje niezwykle powolne testy. Po zastosowaniu do całych testów integracyjnych testowanie tylko niewielkiej części systemu zajmuje około 5 sekund. Mogę sobie wyobrazić, że liczba ta wzrośnie, gdy zasięg wzrośnie.

Jakie byłoby inne podejście do pisania takich testów? Jedną z alternatyw, o której myślę, jest rodzaj zmiennych globalnych (w ramach klasy) i wszystkie metody testowe mają tę zmienną. W rezultacie powstaje i usuwanych jest tylko kilka zamówień; co skutkuje szybszymi testami. Myślę jednak, że wprowadza to większy problem; testy nie są już izolowane, a ich zrozumienie i analiza staje się coraz trudniejsza.

Może się zdarzyć, że testy integracyjne nie będą przeprowadzane tak często, jak testy jednostkowe; dlatego niska wydajność może być do zaakceptowania dla nich. W każdym razie dobrze byłoby wiedzieć, czy ktoś wymyślił alternatywy dla poprawy skalowalności.

Guven
źródło

Odpowiedzi:

6

Spójrz na użycie Hypersonic lub innego DB w pamięci do testowania jednostkowego. Testy będą nie tylko wykonywane szybciej, ale skutki uboczne nie są istotne. (Wycofywanie transakcji po każdym teście umożliwia także uruchomienie wielu testów w tym samym wystąpieniu)

Zmusi Cię to również do tworzenia makiet danych, co jest dobrą rzeczą IMO, ponieważ oznacza, że ​​coś, co dzieje się w produkcyjnej bazie danych, nie może w niewytłumaczalny sposób rozpocząć testów, ORAZ daje punkt wyjścia do „czystej instalacji bazy danych” ”wyglądałoby to, co pomaga, jeśli musisz nagle wdrożyć nową instancję aplikacji bez połączenia z istniejącą witryną produkcyjną.

Tak, sam używam tej metody i tak, to była PITA, aby ustawić PIERWSZY czas, ale to więcej niż się opłaciło w czasie realizacji projektu, który zamierzam zakończyć. Istnieje również szereg narzędzi, które pomagają w tworzeniu makiet danych.

SplinterReality
źródło
Brzmi jak świetny pomysł. Szczególnie podoba mi się ten aspekt izolacji; począwszy od nowej instalacji bazy danych. Wygląda na to, że zajmie to trochę wysiłku, ale po skonfigurowaniu będzie bardzo korzystne. Dzięki.
Guven,
3

Jest to odwieczny problem, z którym wszyscy spotykają się podczas pisania testów integracyjnych.

Idealnym rozwiązaniem, szczególnie jeśli testujesz na produkcji, jest otwarcie transakcji w konfiguracji i wycofanie jej po zakończeniu. Myślę, że powinno to pasować do twoich potrzeb.

Tam, gdzie nie jest to możliwe, na przykład gdy testujesz aplikację z poziomu warstwy klienta, innym rozwiązaniem jest użycie bazy danych na maszynie wirtualnej i zrobienie migawki w konfiguracji i powrót do niej po rozerwaniu (to nie tak długo, jak możesz się spodziewać).

pdr
źródło
Nie mogę uwierzyć, że nie myślałem o wycofaniu transakcji. Wykorzystam ten pomysł jako szybką poprawkę do rozwiązania, ale ostatecznie DB w pamięci brzmi bardzo obiecująco.
Guven,
3

Testy integracyjne należy zawsze przeprowadzać w odniesieniu do konfiguracji produkcji. W twoim przypadku oznacza to, że powinieneś mieć tę samą bazę danych i serwer aplikacji. Oczywiście ze względu na wydajność można zdecydować się na użycie DB w pamięci.

Nigdy nie powinieneś jednak rozszerzać zakresu transakcji. Niektóre osoby sugerowały przejęcie kontroli nad transakcją i wycofanie jej po testach. Gdy to zrobisz, wszystkie jednostki (zakładając, że używasz JPA) pozostaną przywiązane do kontekstu trwałości przez cały czas wykonywania testu. Może to spowodować bardzo nieprzyjemne błędy, które są bardzo trudne do znalezienia .

Zamiast tego należy wyczyścić bazę danych ręcznie po każdym teście przez bibliotekę taką jak JPAUnit lub podobną do tego podejścia (wyczyść wszystkie tabele za pomocą JDBC).

Ze względu na problemy z wydajnością nie należy wykonywać testów integracyjnych dla każdej kompilacji. Pozwól na to serwerowi ciągłej integracji. Jeśli korzystasz z Maven, możesz cieszyć się bezpieczną wtyczką, która pozwala rozdzielić testy na testy jednostkowe i integracyjne.

Nie powinieneś też kpić z niczego. Pamiętaj, że testujesz integrację, tzn. Testujesz zachowanie w środowisku wykonawczym.

BenR
źródło
Dobra odpowiedź. Jeden punkt: Dopuszczalne jest kpienie z niektórych zależności zewnętrznych (np. Wysyłanie wiadomości e-mail). Ale powinieneś raczej wyśmiewać, konfigurując kompletną próbną usługę / serwer, a nie wyśmiewać go w kodzie Java.
sleske
Śleske, zgadzam się z tobą. Zwłaszcza gdy używasz JPA, EJB, JMS lub implementujesz w oparciu o inną specyfikację. Możesz zamienić serwer aplikacji, dostawcę trwałości lub bazę danych. Na przykład możesz użyć osadzonych Glassfish i HSQLDB, aby po prostu skonfigurować i poprawić szybkość (oczywiście możesz wybrać inną certyfikowaną implementację).
BenR
2

Skalowalność

Już wcześniej miałem ten problem, że testy integracyjne trwały zbyt długo i nie były praktyczne, aby pojedynczy programista działał w ciasnej pętli zmian. Niektóre strategie radzenia sobie z tym są:

  • Napisz mniej testów integracyjnych - jeśli masz dobre pokrycie z testami jednostkowymi w systemie, testy integracyjne powinny bardziej skupiać się na (ahem) problemach z integracją, na tym, jak różne komponenty współpracują ze sobą. Są bardziej podobne do testów dymu, które po prostu próbują sprawdzić, czy system nadal działa jako całość. Idealnie więc, twoje testy jednostkowe obejmują większość funkcjonalnych części aplikacji, a testy integracyjne sprawdzają tylko, jak te części oddziałują ze sobą. Nie potrzebujesz tutaj obszernego pokrycia ścieżkami logicznymi, tylko niektóre ścieżki krytyczne przez system.
  • Uruchamiaj tylko podzbiór testów naraz - jako pojedynczy programista pracujący nad pewną funkcjonalnością zwykle masz poczucie, które testy są konieczne, aby objąć tę funkcjonalność. Uruchom tylko te testy, które mają sens, aby objąć twoje zmiany. Frameworki takie jak JUnit pozwalają grupować urządzenia w kategoriach . Utwórz grupy, które zapewnią najlepszy zasięg dla każdej posiadanej funkcji. Oczywiście nadal chcesz uruchomić wszystkie testy integracyjne w pewnym momencie, być może zanim przejdziesz do systemu kontroli źródła i na pewno na serwerze ciągłej integracji.
  • Zoptymalizuj system - Testy integracji w połączeniu z niektórymi testami wydajności mogą dostarczyć niezbędnych danych wejściowych do dostrojenia wolnych części systemu, dzięki czemu testy będą przebiegały szybciej w późniejszym czasie. Może to pomóc odkryć potrzebne indeksy w bazie danych, gadatliwe interfejsy między podsystemami lub inne wąskie gardła wydajności, które wymagają adresowania.
  • Uruchom testy równolegle - jeśli masz dobrą grupę testów ortogonalnych, możesz spróbować uruchomić je równolegle . Uważaj jednak na wymóg ortogonalny , o którym wspomniałem, ale nie chcesz, aby twoje testy stawały sobie nawzajem.

Spróbuj połączyć te techniki, aby uzyskać lepszy efekt.

Jordão
źródło
1
Naprawdę dobre wytyczne; napisanie mniej testów integracyjnych to z pewnością najlepsza rada. Równoległe prowadzenie ich byłoby bardzo dobrą alternatywą; idealny „test”, aby sprawdzić, czy moje testy zostały przeprowadzone pod kątem izolacji.
Guven,
2

Do celów testowych wykorzystaliśmy oparte na plikach wdrożenie bazy danych SQLite (wystarczy skopiować zasób). Dokonano tego, abyśmy mogli również przetestować migracje schematów. O ile mi wiadomo, zmiany schematu nie są transakcyjne, więc nie zostaną wycofane po anulowaniu transakcji. Również brak konieczności polegania na obsłudze transakcji w konfiguracji testowej pozwala poprawnie przetestować zachowanie transakcji z aplikacji.

Carlo Kuip
źródło