Docker – kontener z bazą danych do testów end-to-end

Automatyczne testowanie aplikacji stało się powszechnym i docenianym etapem wytwarzania oprogramowania. Służy nie tylko ochronie przed regresją (psuciem istniejących funkcjonalności), ale także jako punkt odniesienia ‚gdzie jesteśmy’ w obecnej iteracji. Dobre testy w połączeniu z ciągłą integracją w wydatny sposób zwiększają stopień, w jakim panujemy nad wytwarzanym produktem. Poza tym jak powszechnie wiadomo

kod nieprzetestowany z definicji jest zepsuty

Oczywiście napisanie dobrych testów wymaga sporych nakładów pracy. Zebranie dobrych praktyk na ten temat wypełniłoby niejedną książkę.

Testy też są kodem, który wymaga ciągłej pielęgnacji i przy wytwarzaniu których trzeba brać pod uwagę kilka cech decydujących o ich jakości i przydatności. Jedną z nich jest izolacja. Osobne testy nie powinny wpływać na siebie nawzajem. Kolejne wywołania zestawu testów też powinny być od siebie całkowicie niezależne. W przypadku testów jednostkowych zapewnienie separacji jest do osiągnięcia relatywnie tanim kosztem. Wystarczy zadbać o to, by każdy test otrzymywał nowe instancje obiektów, których zachowanie badamy poprzez ostrożnie zbudowany zestaw asercji.

Zgoła inaczej ma się sprawa przy testach wysokopoziomowych, do których między innymi zaliczamy testy end-to-end, zwane inaczej systemowymi. Są to testy, które przechodzą przez CAŁĄ aplikację, zupełnie tak, jak ma to miejsce w środowisku produkcyjnym. Oznacza to, że do ich przeprowadzenia nie wystarczy sam kod źródłowy na komputerze developera. Potrzebne jest całe środowisko odpowiadające w możliwie najbliższym stopniu produkcyjnemu – inaczej wartość tych testów pozostaje co najmniej wątpliwa. W 2017. roku oznacza to konieczność wystawienia kilku do kilkunastu działających procesów – (mikro)serwisów, kilku różnych baz danych oraz dostęp do zewnętrznych usług, z którymi się integrujemy. Z odsieczą przychodzi Docker – lekkie narzędzie do konteneryzacji wykorzystujące natywne funkcje kernela Linuksa. Docker wyraźnie ułatwia i przyspiesza stawianie nowych środowisk – między innymi testowych.

Z sytuacją opisanym w poprzednim akapicie (wystawieniem środowiska do testów) mierzyłem się niedawno. Istniała potrzeba testowania w izolacji pojedynczej usługi z RESTowym API, która do działania potrzebuje dwóch niezależnych baz danych – relacyjnej i nierelacyjnej. Sam koncept zorganizowania takiego zestawu testowego zaczerpnąłem z prelekcji System testing with pytest and docker-py z EuroPythona 2016. Zaimplementowanie analogicznego rozwiązania nie było trudne. Pozwoliło ono uruchamiać zestaw testowy na każdym komputerze developera bez większych problemów.

Niesatysfakcjonujący był jednak czas, jakiego potrzebują kontenery Dockera, aby się podnieść. Trwało to około 2-3 minut. Zdecydowanie zbyt długo dla developera próbującego pracować w cyklu ATDD – feedback z tych testów przychodził zbyt późno, by utrzymać uwagę. Wąskim gardłem okazał się kontener z relacyjną bazą danych. Usługa do działania potrzebowała odpowiednio podciągniętego schematu bazy danych. Oczywiście istniały gotowe skrypty migracji, było ich jednak tak dużo, że wykonanie trwało wieki.

„Tradycyjne” metody w Dockerze jak rozszerzenie kontenera bazy nie wchodziły w grę, ponieważ jego featurem jest stawianie czystej bazy przy starcie i dopiero potem możliwość inicjalizacji przygotowanymi plikami SQL już na działającym serwerze. Innymi słowy, nie ma możliwości przygotowania wprost kontenera z danymi – muszą zostać wprowadzone do systemu bazodanowego od nowa. Przynajmniej jest to fakt, jeżeli chodzi o MySQL i PostgreSQL.

Przygotowanie takiego kontenera zapakowanego danymi do testów jest jednak w pełni uzasadnione. Nie ma potrzeby inicjalizacji bazy od zera przy stawianiu (tracimy cenne sekundy). To, że dane giną razem z kontenerem nie jest problemem – nie są potrzebne nigdy po zakończeniu testowania.

Aby to osiągnąć, trzeba stworzyć własną wariację oryginalnego obrazu mysql:8. Nie chodzi o stworzenie obrazu pochodnego (dziedziczącego z mysql:8), ale przez modyfikacje oryginalnego Dockerfile’a. Następnie na zmodyfikowanym kontenerze przeprowadzić nasze zmiany, a potem utworzyć kolejny obraz korzystając z polecenia docker commit.

Przygotowałem gotowe rozwiązanie dla MySQLa. Zrobienie analogicznego tworu dla Postgresa różni się niewieloma rzeczami i pozostawiam to jako ćwiczenie dla czytelnika.

Przejdę przez najistotniejsze zmiany w stosunku do oryginalnej wersji. Po pierwsze, zmiany w Dockerfile’u:

Na początku zakomentowujemy linię z dyrektywą VOLUME. W oryginalnym obrazie pliki MySQLa zawsze tam trafiały i nie były nigdy zapisywane w samym kontenerze. Domyślnie, o ile przy uruchamianiu kontenera nigdzie tego nie ustawiliśmy, to katalog z danymi był mapowany na dysku hosta w folderach Dockera.

Drugą operacją, dla czystej wygody, jest ustawienie hasła głównego użytkownika bazy roota na „root”.

Jak widać, są to drobne zmiany. Cała magia dzieje się w dołączonym skrypcie:

W linii 2. budowany jest obraz z instrukcji w Dockerfile’u. Linia 3. uruchamia kontener. Dalej w pętli (linie 7-12) próbujemy podłączyć się do wstającej bazy. Gdy to się udaje (linia 13.) przeprowadzamy wszelkie operacje inicjalizujące, jakich potrzebujemy (linie 15 – 20). Może to być tworzenie użytkowników, tabel, wypełnianie ich danymi – albo zwyczajnie uruchomienie opcji migracji z naszej aplikacji podając dane logowania do instancji w kontenerze.

Na koniec w linii 21 poleceniem docker commit zapisujemy wprowadzone w kontenerze zmiany tworząc w ten sposób nowy obraz dostępny od teraz pod nazwą mysql_initialized:latest.

Linia 22. usuwa działający kontener, którego już nie potrzebujemy.

Bazę w nowym obrazie można postawić z polecenia:

I podłączyć się do niej:

Przygotowany kontener nie potrzebuje więcej niż 2 sekundy na pełne podniesienie się i osiągnięcie gotowości do obsługi połączeń. Dla porównania, sam start z inicjalizacją mysql:8 trwa ponad 10-krotnie dłużej. Do tego należałoby doliczyć kolejne sekundy na zmigrowanie schematu bazy i wypełnienie danymi, co nie ma zastosowania przy naszym zoptymalizowanym pod testy obrazie.

Linki

Repozytorium na GitHub z przykładem dla MySQLa

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *