Szukasz sposobu na tworzenie testów w łatwiejszy sposób? Mamy dla Ciebie rozwiązanie! Sprawdź poniższy artykuł i dowiedz się, jak to zrobić.
Nowoczesne tworzenie aplikacji opiera się na jednej prostej zasadzie:
Użyj kompozycji
Łączymy klasy, funkcje i usługi w większe fragmenty oprogramowania. Ten ostatni element jest podstawą mikrousług i architektura sześciokątna. Chcielibyśmy wykorzystać istniejące rozwiązania, zintegrować je z naszym oprogramowaniem i przejść bezpośrednio do rynek.
Chcesz obsługiwać rejestrację kont i przechowywać dane użytkowników? Możesz wybrać jedną z usług OAuth. Może twoja aplikacja oferuje jakiś rodzaj subskrypcji lub płatności? Istnieje wiele usług, które mogą ci w tym pomóc. Potrzebujesz analityki na swojej stronie, ale nie rozumiesz RODO? Nie krępuj się i skorzystaj z jednego z gotowych rozwiązań.
Coś, co sprawia, że rozwój jest tak łatwy z biznesowego punktu widzenia, może przyprawić o ból głowy - w momencie, gdy trzeba napisać prosty test.
Fantastic Beasts: Kolejki, bazy danych i jak je testować
Testowanie jednostkowe jest dość proste. Jeśli będziesz przestrzegać tylko tych zasad, to twoje środowisko testowe i kod są zdrowe. Jakie to zasady?
Łatwość pisania - Testy jednostkowe powinny być łatwe do napisania, ponieważ pisze się ich dużo. Mniejszy wysiłek oznacza więcej napisanych testów.
Czytelny - kod testu powinien być łatwy do odczytania. Test jest historią. Opisuje zachowanie oprogramowania i może być używany jako skrót dokumentacji. Dobry test jednostkowy pomaga naprawić błędy bez debugowania kodu.
Niezawodny - test powinien zakończyć się niepowodzeniem tylko wtedy, gdy w testowanym systemie występuje błąd. Oczywiste? Nie zawsze. Czasami testy przechodzą pomyślnie, jeśli uruchamiasz je pojedynczo, ale kończą się niepowodzeniem, gdy uruchamiasz je jako zestaw. Przechodzą pomyślnie na Twojej maszynie, ale kończą się niepowodzeniem na CI (Działa na moim komputerze). Dobry test jednostkowy ma tylko jedną przyczynę niepowodzenia.
Szybko - testy powinny być szybkie. Przygotowanie do uruchomienia, uruchomienie i samo wykonanie testu powinno być bardzo szybkie. W przeciwnym razie będziesz je pisać, ale nie uruchamiać. Wolne testy oznaczają utratę koncentracji. Czekasz i patrzysz na pasek postępu.
Niezależny - wreszcie, test powinien być niezależny. Ta zasada wynika z poprzednich. Tylko prawdziwie niezależne testy mogą stać się jednostką. Nie kolidują one ze sobą, mogą być uruchamiane w dowolnej kolejności, a potencjalne awarie nie zależą od wyników innych testów. Niezależność oznacza również brak zależności od jakichkolwiek zewnętrznych zasobów, takich jak bazy danych, usługi przesyłania wiadomości lub system plików. Jeśli musisz komunikować się z zewnętrznymi zasobami, możesz użyć makiet, stubów lub manekinów.
Wszystko staje się skomplikowane, gdy chcemy napisać testy integracyjne. Nie jest źle, jeśli chcemy przetestować kilka usług razem. Ale kiedy musimy przetestować usługi, które korzystają z zewnętrznych zasobów, takich jak bazy danych lub usługi przesyłania wiadomości, wtedy prosimy o kłopoty.
Aby uruchomić test, należy zainstalować...
Wiele lat temu, gdy chcieliśmy wykonać testy integracyjne i wykorzystać np. bazy danych, mieliśmy dwie opcje:
Możemy zainstalować bazę danych lokalnie. Skonfiguruj schemat i połącz się z naszego testu;
Możemy połączyć się z istniejącą instancją "gdzieś w przestrzeni".
Oba rozwiązania mają swoje plusy i minusy. Ale oba wprowadzają dodatkowe poziomy złożoności. Czasami była to złożoność techniczna wynikająca z charakterystyki niektórych narzędzi, np. instalacja i zarządzanie Oracle DB na lokalnym hoście. Czasami była to niedogodność w procesie, np. konieczność uzgodnienia z testerem zespół o wykorzystaniu JMS... za każdym razem, gdy chcesz uruchomić testy.
Kontenery na ratunek
W ciągu ostatnich 10 lat idea konteneryzacji zyskała uznanie w branży. Naturalną decyzją było więc wybranie kontenerów jako rozwiązania dla naszej kwestii testów integracyjnych. Jest to proste, czyste rozwiązanie. Wystarczy uruchomić kompilację procesu i wszystko działa! Nie możesz w to uwierzyć? Spójrz na tę prostą konfigurację kompilacji maven:
com.dkanejs.maven.plugins
docker-compose-maven-plugin
4.0.0
up
test-compile
up
${projekt.basedir}/docker-compose.yml
true.
down
post-integration-test
down
${project.basedir}/docker-compose.yml
true.
I docker-compose.yml Plik też wygląda całkiem nieźle!
Powyższy przykład jest bardzo prosty. Tylko jedna baza danych postgres, pgAdmin i to wszystko. Po uruchomieniu
bash
$ mvn clean verify
następnie wtyczka maven uruchamia kontenery, a po testach je wyłącza. Problemy zaczynają się, gdy projekt się rozrasta, a nasz plik compose również rośnie. Za każdym razem trzeba będzie uruchamiać wszystkie kontenery i będą one żyły przez cały czas trwania kompilacji. Można nieco poprawić sytuację zmieniając konfigurację wykonywania pluginów, ale to nie wystarczy. W najgorszym przypadku kontenery wyczerpią zasoby systemowe przed rozpoczęciem testów!
I nie jest to jedyny problem. Nie można uruchomić pojedynczego testu integracyjnego z IDE. Wcześniej trzeba ręcznie uruchomić kontenery. Co więcej, następne uruchomienie mavena zburzy te kontenery (spójrz na w dół wykonanie).
Więc to rozwiązanie jest jak duży statek towarowy. Jeśli wszystko działa dobrze, to jest w porządku. Każde nieoczekiwane lub nietypowe zachowanie prowadzi nas do pewnego rodzaju katastrofy.
Kontenery testowe - uruchamianie kontenerów z testów
Ale co by było, gdybyśmy mogli uruchamiać nasze kontenery z testów? Ten pomysł wygląda dobrze i jest już wdrażany. Pojemniki testowePonieważ mówimy o tym projekcie, oto rozwiązanie naszych problemów. Nie jest idealne, ale nikt nie jest idealny.
To jest Java która obsługuje testy JUnit i Spock, zapewniając lekkie i łatwe sposoby uruchamiania kontenera Docker. Przyjrzyjmy się temu i napiszmy trochę kodu!
Wymagania wstępne i konfiguracja
Zanim zaczniemy, musimy sprawdzić naszą konfigurację. Pojemniki testowe potrzeba:
Docker w wersji v17.09,
Java w wersji minimum 1.8,
Dostęp do sieci, zwłaszcza do docker.hub.
Więcej informacji na temat wymagań dla określonego systemu operacyjnego i CI można znaleźć na stronie w dokumentacja.
Teraz nadszedł czas, aby dodać kilka linii do pom.xml.
org.testcontainers
testcontainers-bom
${testcontaines.version}
pom
import
org.postgresql
postgresql
runtime
org.testcontainers
postgresql
test
org.testcontainers
junit-jupiter
test
.
Używam Pojemniki testowe wersja 1.17.3, ale nie krępuj się użyć najnowszego.
Testy z kontenerem Postgres
Pierwszym krokiem jest przygotowanie instancji kontenera. Można to zrobić bezpośrednio w teście, ale lepiej wygląda niezależna klasa.
public class Postgres13TC extends PostgreSQLContainer {
private static final Postgres13TC TC = new Postgres13TC();
private Postgres13TC() {
super("postgres:13.2");
}
public static Postgres13TC getInstance() {
return TC;
}
@Override
public void start() {
super.start();
System.setProperty("DB_URL", TC.getJdbcUrl());
System.setProperty("DB_USERNAME", TC.getUsername());
System.setProperty("DB_PASSWORD", TC.getPassword());
}
@Override
public void stop() {
// nic nie rób. To jest współdzielona instancja. Niech JVM zajmie się tą operacją.
}
}
Na początku testów utworzymy instancję klasy Postgres13TC. Ta klasa może obsługiwać informacje o naszym kontenerze. Najważniejsze są tutaj ciągi połączenia z bazą danych i dane uwierzytelniające. Teraz nadszedł czas na napisanie bardzo prostego testu.
Używam tutaj JUnit 5. Adnotacja @Testcontainers jest częścią rozszerzeń, które kontrolują kontenery w środowisku testowym. Znajdują one wszystkie pola z @Kontener adnotacja i odpowiednio kontenery start i stop.
Testy z użyciem Spring Boot
Jak wspomniałem wcześniej, w projekcie wykorzystuję Spring Boot. W tym przypadku musimy napisać trochę więcej kodu. Pierwszym krokiem jest stworzenie dodatkowej klasy konfiguracyjnej.
Klasa ta zastępuje istniejące właściwości wartościami z klasy pojemnik testowy. Pierwsze trzy właściwości są standardowymi właściwościami Spring. Kolejne pięć to dodatkowe, niestandardowe właściwości, które mogą być używane do konfigurowania innych zasobów i rozszerzeń, takich jak np. liquibase:
Teraz nadszedł czas na zdefiniowanie prostego testu integracyjnego.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureTestDatabase(replace = NONE)
@ContextConfiguration(initializers = ContainerInit.class)
@Testcontainers
class DummyRepositoryTest {
@Autowired
private DummyRepository;
@Test
void shouldReturnDummy() {
var byId = dummyRepository.getById(10L);
var expected = new Dummy();
expected.setId(10L);
assertThat(byId).completes().emitsCount(1).emits(expected);
}
}
Mamy tutaj kilka dodatkowych adnotacji.
@SpringBootTest(webEnvironment = RANDOM_PORT) - oznacza test jako test Spring Boot i uruchamia kontekst Spring.
@AutoConfigureTestDatabase(replace = NONE) - te adnotacje mówią, że rozszerzenie testu wiosennego nie powinno zastępować konfiguracji bazy danych postgres konfiguracją H2 w konfiguracji pamięci.
@ContextConfiguration(initializers = ContainerInit.class) - dodatkowy kontekst wiosenny konfiguracja, w której ustawiamy właściwości z Pojemniki testowe.
@Testcontainers - Jak wspomniano wcześniej, adnotacja ta kontroluje cykl życia kontenera.
W tym przykładzie używam repozytoriów reaktywnych, ale działa to tak samo ze zwykłymi repozytoriami JDBC i JPA.
Teraz możemy uruchomić ten test. Jeśli jest to pierwsze uruchomienie, silnik musi pobrać obrazy z docker.hub. To może chwilę potrwać. Następnie zobaczymy, że uruchomione zostały dwa kontenery. Jednym z nich jest postgres, a drugim kontroler Testcontainers. Ten drugi kontener zarządza uruchomionymi kontenerami i nawet jeśli JVM nieoczekiwanie się zatrzyma, wyłącza kontenery i czyści środowisko.
Podsumujmy
Pojemniki testowe to bardzo łatwe w użyciu narzędzia, które pomagają nam tworzyć testy integracyjne wykorzystujące kontenery Docker. Daje nam to większą elastyczność i zwiększa szybkość rozwoju. Odpowiednia konfiguracja testów skraca czas potrzebny na wdrożenie nowych programistów. Nie muszą oni ustawiać wszystkich zależności, wystarczy, że uruchomią napisane testy z wybranymi plikami konfiguracyjnymi.