Jaka jest najlepsza strategia do testowania aplikacji opartych na bazie danych?


346

Pracuję z wieloma aplikacjami internetowymi, które są obsługiwane przez bazy danych o różnym stopniu złożoności w backend. Zazwyczaj istnieje warstwa ORM oddzielna od logiki biznesowej i prezentacji. To sprawia, że ​​testowanie jednostkowe logiki biznesowej jest dość proste; rzeczy mogą być zaimplementowane w modułach dyskretnych, a wszelkie dane potrzebne do testu mogą zostać sfałszowane poprzez wyśmiewanie obiektów.

Jednak testowanie ORM i samej bazy danych zawsze było obarczone problemami i kompromisami.

Przez lata wypróbowałem kilka strategii, z których żadna nie była dla mnie satysfakcjonująca.

  • Załaduj testową bazę danych ze znanymi danymi. Przeprowadź testy w stosunku do ORM i potwierdź, że odpowiednie dane powracają. Wadą jest to, że testowa baza danych musi nadążać za wszelkimi zmianami schematu w bazie danych aplikacji i może nie być zsynchronizowana. Opiera się również na sztucznych danych i może nie ujawniać błędów, które pojawiają się z powodu głupiego wkładu użytkownika. Wreszcie, jeśli testowa baza danych jest mała, nie ujawni ona nieefektywności takich jak brakujący indeks. (OK, ten ostatni nie jest tak naprawdę, do czego należy stosować testy jednostkowe, ale to nie boli).

  • Załaduj kopię produkcyjnej bazy danych i przetestuj ją. Problem polega na tym, że w danym momencie możesz nie mieć pojęcia, co znajduje się w produkcyjnej bazie danych; Twoje testy mogą wymagać przepisania, jeśli dane zmienią się z czasem.

Niektóre osoby zauważyły, że obie te strategie opierają się na konkretnych danych, a test jednostkowy powinien przetestować tylko funkcjonalność. W tym celu widziałem sugerowane:

  • Użyj fałszywego serwera bazy danych i sprawdź tylko, czy ORM wysyła poprawne zapytania w odpowiedzi na dane wywołanie metody.

Jakie strategie zastosowałeś do testowania aplikacji opartych na bazie danych, jeśli takie istnieją? Co działało najlepiej dla Ciebie?


Myślę, że nadal powinieneś mieć indeksy baz danych w środowisku testowym dla przypadków takich jak indeksy unikalne.
dtc

I pesonally nie przeszkadza to pytanie tutaj, ale jeśli idziemy według zasad, to pytanie nie jest dla stackoverflow raczej jest dla softwareengineering.stackexchange internetowej.
ITExpert

Odpowiedzi:


155

Właściwie zastosowałem twoje pierwsze podejście z pewnym powodzeniem, ale w nieco inny sposób, który moim zdaniem rozwiązałby niektóre z twoich problemów:

  1. Zachowaj cały schemat i skrypty do tworzenia go pod kontrolą źródła, aby każdy mógł utworzyć bieżący schemat bazy danych po wymeldowaniu. Ponadto przechowuj przykładowe dane w plikach danych, które są ładowane przez część procesu kompilacji. Gdy odkryjesz dane, które powodują błędy, dodaj je do przykładowych danych, aby sprawdzić, czy błędy nie pojawiają się ponownie.

  2. Użyj serwera ciągłej integracji, aby zbudować schemat bazy danych, załadować przykładowe dane i uruchomić testy. W ten sposób synchronizujemy naszą testową bazę danych (odbudowując ją przy każdym uruchomieniu testowym). Chociaż wymaga to, aby serwer CI miał dostęp i własność własnej dedykowanej instancji bazy danych, mówię, że zbudowanie naszego schematu db 3 razy dziennie znacznie pomogło znaleźć błędy, które prawdopodobnie nie zostałyby wykryte tuż przed dostarczeniem (jeśli nie później) ). Nie mogę powiedzieć, że odbudowuję schemat przed każdym zatwierdzeniem. Czy ktoś? Dzięki takiemu podejściu nie będziesz musiał (no może powinniśmy, ale nie jest to wielka sprawa, jeśli ktoś zapomni).

  3. W mojej grupie wprowadzanie danych przez użytkownika odbywa się na poziomie aplikacji (nie db), więc jest to testowane za pomocą standardowych testów jednostkowych.

Ładowanie kopii produkcyjnej bazy danych:
takie podejście zastosowano przy mojej ostatniej pracy. To była ogromna bolesna przyczyna kilku problemów:

  1. Kopia przestanie być aktualna z wersji produkcyjnej
  2. Zmiany zostaną wprowadzone w schemacie kopii i nie zostaną rozpowszechnione w systemach produkcyjnych. W tym momencie mielibyśmy rozbieżne schematy. Nie śmieszne.

Mocking Database Server:
Robimy to również w mojej obecnej pracy. Po każdym zatwierdzeniu wykonujemy testy jednostkowe kodu aplikacji, do którego zostały wstrzyknięte pozorne akcesory db. Następnie trzy razy dziennie wykonujemy pełną kompilację db opisaną powyżej. Zdecydowanie polecam oba podejścia.


37
Ładowanie produkcyjnej kopii bazy danych ma również wpływ na bezpieczeństwo i prywatność. Gdy stanie się duży, jego skopiowanie i umieszczenie w środowisku deweloperskim może być wielką sprawą.
WW.

szczerze mówiąc, to ogromny ból. Jestem nowy w testowaniu, a także napisałem orm, który chcę przetestować. Użyłem już twojej pierwszej metody, ale przeczytałem, że nie tworzy ona jednostki testowej. Używam określonej funkcjonalności silnika db, więc kpiny z DAO będą trudne. Myślę, że źle używam mojej obecnej metody, ponieważ działa, a inni ją używają. Zautomatyzowane testy rock btw. Dzięki.
frostymarvelous

2
Zarządzam dwoma różnymi dużymi projektami, w jednym z nich to podejście było idealne, ale mieliśmy wiele problemów z wdrożeniem tego w innym projekcie. Myślę więc, że to zależy od tego, jak łatwo można odtworzyć schemat za każdym razem, aby wykonać testy, obecnie pracuję nad znalezieniem nowego rozwiązania tego ostatniego problemu.
Krzyż

2
W takim przypadku zdecydowanie warto skorzystać z narzędzia do wersjonowania bazy danych, takiego jak Roundhouse - coś, co może uruchamiać migracje. Można to uruchomić na dowolnej instancji DB i upewnić się, że schematy są aktualne. Ponadto podczas pisania skryptów migracyjnych należy również napisać dane testowe - synchronizując migracje i dane.
jedd.ahyoung

lepiej używaj łatania małp i
kpienia

56

Zawsze uruchamiam testy przeciwko DB w pamięci (HSQLDB lub Derby) z następujących powodów:

  • Sprawia, że ​​zastanawiasz się, które dane zachować w testowej bazie danych i dlaczego. Po prostu wciągnięcie produkcyjnej bazy danych do systemu testowego tłumaczy się: „Nie mam pojęcia, co robię i dlaczego, a jeśli coś się psuje, to nie byłem ja !!” ;)
  • Daje to pewność, że bazę danych można odtworzyć przy niewielkim wysiłku w nowym miejscu (na przykład, gdy musimy replikować błąd z produkcji)
  • Ogromnie pomaga w jakości plików DDL.

DB w pamięci jest ładowany świeżymi danymi po rozpoczęciu testów, a po większości testów wywołuję ROLLBACK, aby zachować stabilność. ZAWSZE utrzymuj dane w testowej bazie danych stabilne! Jeśli dane cały czas się zmieniają, nie można testować.

Dane są ładowane z SQL, szablonu DB lub zrzutu / kopii zapasowej. Wolę zrzuty, jeśli są w czytelnym formacie, ponieważ mogę umieścić je w VCS. Jeśli to nie działa, używam pliku CSV lub XML. Jeśli muszę załadować ogromne ilości danych ... nie robię tego. Nigdy nie musisz ładować ogromnych ilości danych :) Nie do testów jednostkowych. Testy wydajności to kolejny problem i obowiązują inne zasady.


1
Czy szybkość jest jedynym powodem używania (konkretnie) DB w pamięci?
rinogo

2
Wydaje mi się, że kolejną zaletą może być „wyrzucona” natura - nie trzeba po sobie sprzątać; po prostu zabij DB w pamięci. (Ale istnieją inne sposoby osiągnięcia tego celu, takie jak wspomniane podejście ROLLBACK)
rinogo

1
Zaletą jest to, że każdy test może indywidualnie wybrać strategię. Mamy testy, które działają w wątkach potomnych, co oznacza, że ​​Spring zawsze zatwierdza dane.
Aaron Digulla

@Aaron: My także stosujemy tę strategię. Chciałbym wiedzieć, jaka jest twoja strategia, aby twierdzić, że model w pamięci ma taką samą strukturę jak prawdziwy db?
Guillaume

1
@Guillaume: Tworzę wszystkie bazy danych z tych samych plików SQL. H2 jest do tego świetny, ponieważ obsługuje większość cech charakterystycznych SQL głównych baz danych. Jeśli to nie zadziała, używam filtra, który pobiera oryginalny SQL i konwertuje go na SQL dla bazy danych w pamięci.
Aaron Digulla

14

Zadaję to pytanie od dłuższego czasu, ale myślę, że nie ma na to srebrnej kuli.

Obecnie robię drwiny z obiektów DAO i przechowuję w pamięci dobrą kolekcję obiektów reprezentujących ciekawe przypadki danych, które mogłyby istnieć w bazie danych.

Główny problem, jaki dostrzegam w tym podejściu, polega na tym, że pokrywasz tylko kod, który wchodzi w interakcję z twoją warstwą DAO, ale nigdy nie testujesz samego DAO, i z mojego doświadczenia wynika, że ​​na tej warstwie również występuje wiele błędów. Prowadzę również kilka testów jednostkowych, które działają na bazie danych (w celu użycia TDD lub szybkiego testowania lokalnego), ale testy te nigdy nie są uruchamiane na moim serwerze ciągłej integracji, ponieważ nie przechowujemy bazy danych w tym celu, a ja zdaniem testy uruchomione na serwerze CI powinny być niezależne.

Innym podejściem, które uważam za bardzo interesujące, ale nie zawsze warte uwagi, ponieważ jest trochę czasochłonne, jest stworzenie tego samego schematu, którego używasz do produkcji we wbudowanej bazie danych, która działa tylko w ramach testów jednostkowych.

Chociaż nie ma wątpliwości, że to podejście poprawia zasięg, ma kilka wad, ponieważ musisz być jak najbliżej ANSI SQL, aby działał zarówno z bieżącym DBMS, jak i wbudowanym zamiennikiem.

Bez względu na to, co Twoim zdaniem jest bardziej odpowiednie dla Twojego kodu, istnieje kilka projektów, które mogą to ułatwić, na przykład DbUnit .


13

Nawet jeśli istnieją narzędzia, które pozwalają na mock bazy danych w taki czy inny sposób (np jOOQ „s MockConnection, które można zobaczyć w tej odpowiedzi - Zastrzeżone, pracuję dla dostawcy jOOQ za), I radzę nie wyśmiewać większych baz danych z kompleksem zapytania.

Nawet jeśli chcesz tylko przetestować integrację ORM, pamiętaj, że ORM wysyła do bazy danych bardzo złożoną serię zapytań, które mogą się różnić

  • składnia
  • złożoność
  • zamówienie (!)

Wyśmiewanie się z wszystkiego, aby uzyskać sensowne dane pozorne, jest dość trudne, chyba że w rzeczywistości buduje się małą bazę danych wewnątrz tego modelu, która interpretuje przesyłane instrukcje SQL. Powiedziawszy to, skorzystaj ze znanej bazy danych testów integracji, którą możesz łatwo zresetować za pomocą dobrze znanych danych, na podstawie której możesz uruchomić testy integracji.


5

Używam pierwszego (uruchamianie kodu na testowej bazie danych). Jedynym istotnym problemem, jaki widzę w tym podejściu, jest możliwość zsynchronizowania schematów, z którą mam do czynienia, utrzymując numer wersji w mojej bazie danych i wprowadzając wszystkie zmiany schematu za pomocą skryptu, który stosuje zmiany dla każdego przyrostu wersji.

Najpierw wprowadzam również wszystkie zmiany (w tym schemat bazy danych) w moim środowisku testowym, więc jest odwrotnie: po przejściu wszystkich testów zastosuj aktualizacje schematu na hoście produkcyjnym. W systemie deweloperskim przechowuję również osobną parę baz danych testowania i aplikacji, dzięki czemu mogę sprawdzić, czy aktualizacja db działa poprawnie, zanim dotknę prawdziwych pól produkcyjnych.


3

Korzystam z pierwszego podejścia, ale nieco innego, co pozwala rozwiązać wspomniane problemy.

Wszystko, co jest potrzebne do uruchomienia testów dla DAO, znajduje się pod kontrolą źródła. Zawiera schemat i skrypty do tworzenia bazy danych (doker jest do tego bardzo dobry). Jeśli można użyć wbudowanego DB - używam go do szybkości.

Ważną różnicą w stosunku do innych opisanych podejść jest to, że dane wymagane do testu nie są ładowane ze skryptów SQL lub plików XML. Wszystko (z wyjątkiem niektórych danych słownikowych, które są faktycznie stałe) jest tworzone przez aplikację przy użyciu funkcji / klas narzędzi.

Głównym celem jest wykorzystanie danych do testów

  1. bardzo blisko testu
  2. jawne (użycie plików SQL do danych sprawia, że ​​bardzo problematyczne jest sprawdzenie, z którego fragmentu danych korzysta dany test)
  3. izolować testy od niepowiązanych zmian.

Zasadniczo oznacza to, że narzędzia te pozwalają deklaratywnie określać tylko rzeczy niezbędne dla samego testu w teście i pomijać rzeczy nieistotne.

Aby dać pojęcie o tym, co to znaczy w praktyce, rozważ test dla DAO, który działa z napisami Comments do Posts napisanymi przez Authors. Aby przetestować operacje CRUD dla takiego DAO, niektóre dane powinny zostać utworzone w DB. Test wyglądałby następująco:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Ma to kilka zalet w porównaniu ze skryptami SQL lub plikami XML z danymi testowymi:

  1. Utrzymanie kodu jest znacznie łatwiejsze (dodanie obowiązkowej kolumny, na przykład w niektórych jednostkach, do których odwołuje się wiele testów, takich jak Autor, nie wymaga zmiany wielu plików / rekordów, a jedynie zmiany w kreatorze i / lub fabryce)
  2. Dane wymagane przez konkretny test są opisane w samym teście, a nie w innym pliku. Ta bliskość jest bardzo ważna dla zrozumienia testu.

Wycofywanie kontra zatwierdzanie

Uważam, że wygodniej jest, gdy testy wykonują zatwierdzenie po ich wykonaniu. Po pierwsze, niektóre efekty (na przykładDEFERRED CONSTRAINTS ) nie można sprawdzić, jeśli nigdy się nie wydarzy. Po drugie, gdy test się nie powiedzie, dane można sprawdzić w bazie danych, ponieważ nie są one przywracane przez wycofanie.

Oczywiście ma to wadę, że test może spowodować uszkodzenie danych, co doprowadzi do niepowodzenia w innych testach. Aby sobie z tym poradzić, próbuję wyizolować testy. W powyższym przykładzie każdy test może tworzyć nowe, Authora wszystkie inne podmioty są z nim powiązane, więc kolizje są rzadkie. Aby poradzić sobie z pozostałymi niezmiennikami, które mogą być potencjalnie zepsute, ale nie mogą być wyrażone jako ograniczenie poziomu DB, używam niektórych kontroli programowych pod kątem błędnych warunków, które mogą być uruchamiane po każdym pojedynczym teście (i są uruchamiane w CI, ale zwykle są wyłączane lokalnie dla wydajności powody).


Jeśli wysyłasz bazę danych za pomocą encji i skryptów orm zamiast skryptów SQL, ma tę zaletę, że kompilator zmusi cię do naprawy kodu źródłowego, jeśli wprowadzisz zmiany w modelu. Ma zastosowanie tylko wtedy, gdy używasz języka statycznego.
daramasala

Dla wyjaśnienia: czy używasz funkcji / klas narzędziowych w całej aplikacji, czy tylko do testów?
Ella

@Ella te funkcje narzędzi zwykle nie są potrzebne poza kodem testowym. Pomyśl na przykład o PostBuilder.post(). Generuje pewne wartości dla wszystkich obowiązkowych atrybutów postu. Nie jest to potrzebne w kodzie produkcyjnym.
Roman Konoval

2

W przypadku projektu opartego na JDBC (bezpośrednio lub pośrednio, np. JPA, EJB, ...) możesz wykonać makietę nie całej bazy danych (w takim przypadku lepiej byłoby użyć testowej bazy danych na prawdziwym RDBMS), ale tylko makietę na poziomie JDBC .

Zaletą jest abstrakcja, która przychodzi w ten sposób, ponieważ dane JDBC (zestaw wyników, liczba aktualizacji, ostrzeżenie, ...) są takie same bez względu na backend: twoja baza danych prod, testowa baza danych lub tylko niektóre dane makiety dostarczone dla każdego testu walizka.

Po skonfigurowaniu połączenia JDBC dla każdego przypadku nie ma potrzeby zarządzania testową bazą danych (czyszczenie, tylko jeden test na raz, przeładowywanie urządzeń, ...). Każde połączenie makiety jest izolowane i nie ma potrzeby czyszczenia. W każdym przypadku testowym udostępniane są tylko minimalne wymagane urządzenia, aby wyodrębnić wymianę JDBC, co pomaga uniknąć złożoności zarządzania całym testowym plikiem db.

Acolyte to moja platforma, która zawiera sterownik JDBC i narzędzie do tego rodzaju makiety: http://acolyte.eu.org .

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.