Automatyczne testowanie mikroserwisów w Dockerze dla ciągłej integracji

W projektach związanych z rozwojem architektury mikrousług, CI/CD przekształca się z miłej możliwości w kategorię pilnej potrzeby. Automatyczne testowanie jest nieodłączną częścią ciągłej integracji, na którą odpowiednie podejście może zapewnić zespołowi wiele przyjemnych wieczorów z rodziną i przyjaciółmi. W przeciwnym razie projekt ryzykuje, że nigdy nie zostanie zakończony.

Można pokryć cały kod mikrousługi testami jednostkowymi z obiektami zastępczymi, ale to jedynie częściowo rozwiązuje problem i pozostawia wiele pytań i trudności, szczególnie podczas testowania pracy z danymi. Jak zawsze, najbardziej palącym problemem jest testowanie spójności danych w relacyjnym DB, testowanie pracy z usługami chmurowymi oraz błędne założenia przy pisaniu obiektów zastępczych.

To wszystko i nieco więcej można rozwiązać, testując cały mikrousługę w kontenerze Docker. Niezaprzeczalną zaletą zapewnienia ważności testów jest to, że testy są przeprowadzane na tych samych obrazach Docker, które trafiają do produkcji.

Automatyzacja takiego podejścia stwarza szereg problemów, których rozwiązanie opisano nieco niżej:

  • konflikty równoległych zadań w jednym hoście Docker;
  • konflikty identyfikatorów w bazie danych podczas iteracji testowej;
  • oczekiwanie na gotowość mikrousług;
  • zbieranie i wyprowadzanie logów do zewnętrznych systemów;
  • testowanie wychodzących żądań HTTP;
  • testowanie WebSocketów (z użyciem SignalR);
  • testowanie uwierzytelniania i autoryzacji OAuth.

To artykuł inspirowany mojego wystąpienia na SECR 2019. Więc dla tych, którzy nie lubią czytać, oto nagranie wystąpienia.

Automatyczne testowanie mikroserwisów w Dockerze dla ciągłej integracji

W artykule omówię, jak za pomocą skryptu uruchomić w Dockerze testowaną usługę, bazę danych i usługi Amazon AWS, następnie testy w Postmanie, a po ich zakończeniu zatrzymać i usunąć utworzone kontenery. Testy są wykonywane przy każdej zmianie kodu. W ten sposób upewniamy się, że każda wersja poprawnie działa z bazą danych i usługami AWS.

Ten sam skrypt uruchamiają zarówno sami deweloperzy na swoich desktopach z systemem Windows, jak i serwer Gitlab CI pod Linuksem.

Aby wdrożenie nowych testów było uzasadnione, nie powinno wymagać instalacji dodatkowych narzędzi ani na komputerze dewelopera, ani na serwerze, gdzie testy są uruchamiane po commicie. Docker rozwiązuje ten problem.

Test powinien działać na lokalnym serwerze z następujących powodów:

  • Sieć nigdy nie jest całkowicie niezawodna. Z tysiąca zapytań jedno może nie przejść;
    W takim przypadku automatyczny test się nie powiedzie, praca zostanie zatrzymana, będziemy musieli szukać przyczyny w logach;
  • Niektóre zewnętrzne serwisy nie dopuszczają zbyt częstych zapytań.

Ponadto niezalecane jest używanie środowiska testowego, ponieważ:

  • Złamanie środowiska może nastąpić nie tylko z powodu złego kodu działającego na nim, ale także przez dane, których prawidłowy kod nie może przetworzyć;
  • Niezależnie od tego, jak bardzo staramy się cofnąć wszystkie zmiany wprowadzone przez test, w trakcie samego testu coś może pójść nie tak (w przeciwnym razie po co przeprowadzać test?).

O projekcie i organizacji procesu

Nasza firma opracowała mikroserwisową aplikację webową działającą w Dockerze w chmurze Amazon AWS. W projekcie wykorzystywano już testy jednostkowe, jednak często pojawiały się błędy, których te testy nie wykrywały. Konieczne było przetestowanie całego mikroserwisu wraz z bazą danych i usługami Amazon.

W projekcie stosowany jest standardowy proces ciągłej integracji, obejmujący testowanie mikroserwisu przy każdym commicie. Po przydzieleniu zadania programista wprowadza zmiany w mikroserwisie, testuje je ręcznie i uruchamia wszystkie dostępne testy automatyczne. W razie potrzeby programista modyfikuje testy. Jeśli nie wykryto problemów, następuje commit do gałęzi tego zadania. Po każdym commicie na serwerze automatycznie uruchamiane są testy. Po udanym przeglądzie następuje scalanie z gałęzią główną i uruchamianie testów automatycznych na niej. Jeśli testy na gałęzi głównej przechodzą, usługa jest automatycznie aktualizowana w środowisku testowym na Amazon Elastic Container Service (środowisko testowe). Środowisko to jest niezbędne wszystkim programistom i testerom, a jego łamanie jest niewskazane. Testerzy w tym środowisku sprawdzają poprawkę lub nową funkcję, wykonując testy ręczne.

Architektura projektu

Automatyczne testowanie mikroserwisów w Dockerze dla ciągłej integracji

Aplikacja składa się z ponad dziesięciu usług. Niektóre z nich są napisane w .NET Core, a inne w NodeJs. Każda usługa działa w kontenerze Docker w Amazon Elastic Container Service. Każda z nich ma swoją własną bazę Postgres, a niektóre z dodatkową bazą Redis. Nie ma wspólnych baz. Jeśli kilku usługom potrzebne są te same dane, to w momencie ich zmiany są one przekazywane każdej z tych usług za pośrednictwem SNS (Simple Notification Service) i SQS (Amazon Simple Queue Service), a usługi zapisują je w swoich odrębnych bazach.

SQS i SNS

SQS pozwala na przesyłanie wiadomości do kolejki i odczytywanie wiadomości z kolejki za pośrednictwem protokołu HTTPS.

Jeśli kilka usług odczytuje jedną kolejkę, to każda wiadomość trafia tylko do jednej z nich. Jest to przydatne przy uruchamianiu wielu instancji jednej usługi w celu rozłożenia obciążenia pomiędzy nimi.

Jeśli potrzebujesz, aby każda wiadomość była dostarczana do kilku usług, każda z odbiorców musi mieć swoją własną kolejkę, a do duplikacji wiadomości do kilku kolejek potrzebne jest SNS.

W SNS tworzysz temat i subskrybujesz do niego, na przykład, kolejkę SQS. Do tematu można wysyłać wiadomości. Wówczas wiadomość jest wysyłana do każdej kolejki, która jest subskrybowana do tego tematu. W SNS nie ma metody do odczytywania wiadomości. Jeśli w trakcie debugowania lub testowania potrzebujesz dowiedzieć się, co jest wysyłane do SNS, możesz utworzyć kolejkę SQS, subskrybować ją do odpowiedniego tematu i odczytać kolejkę.

Automatyczne testowanie mikroserwisów w Dockerze dla ciągłej integracji

API Gateway

Większość usług nie jest bezpośrednio dostępna z internetu. Dostęp jest realizowany za pośrednictwem API Gateway, który sprawdza uprawnienia dostępu. To również nasza usługa, i dla niej także są testy.

Powiadomienia w czasie rzeczywistym

Aplikacja wykorzystuje SignalR, aby pokazywać użytkownikowi powiadomienia w czasie rzeczywistym. Jest to zrealizowane w usłudze powiadomień. Jest dostępna bezpośrednio z internetu i samodzielnie działa z OAuth, ponieważ wbudowanie wsparcia dla WebSocketów w Gateway okazało się niecelowe w porównaniu z integracją OAuth i usługi powiadomień.

Znany sposób testowania

Testy jednostkowe podmieniają taką rzecz jak baza danych na obiekty mock. Jeśli mikroserwis na przykład próbuje utworzyć wpis w tabeli z kluczem zewnętrznym, a wpis, do którego ten klucz się odnosi, nie istnieje, to zapytanie nie może zostać zrealizowane. Testy jednostkowe nie mogą tego wykryć.

W artykuł od Microsoft zaleca używanie bazy in-memory i wprowadzać obiekty mock.

Baza danych in-memory to jeden z systemów zarządzania bazami danych wspieranych przez Entity Framework. Została stworzona specjalnie do celów testowych. Dane w tej bazie są przechowywane tylko do momentu zakończenia procesu, który jej używa. Nie ma potrzeby tworzenia tabel, a integralność danych nie jest sprawdzana.

Obiekty mockują tylko te klasy, które programista testu rozumie i może zamienić.

W artykule od Microsoftu nie podano, jak automatycznie uruchomić Postgresa i wykonać migrację podczas uruchamiania testu. Moje rozwiązanie to robi, a dodatkowo nie dodaje żadnego kodu specjalnie do mikroserwisu w celach testowych.

Przechodzimy do rozwiązania

W trakcie rozwoju stało się jasne, że testy jednostkowe są niewystarczające, aby na czas wykrywać wszystkie problemy, dlatego postanowiono podejść do tego zagadnienia z innej strony.

Konfiguracja środowiska testowego

Pierwszym zadaniem jest uruchomienie środowiska testowego. Kroki, które są konieczne do uruchomienia mikroserwisu:

  • Skonfigurować testowany serwis na lokalne środowisko, w zmiennych środowiskowych podać dane do połączenia z bazą i AWS;
  • Uruchomić Postgresa i wykonać migrację przy użyciu Liquibase.
    W relacyjnych systemach zarządzania bazami danych, zanim zapiszemy dane w bazie, należy stworzyć schemat danych, mówiąc prościej, tabele. Przy aktualizacji aplikacji tabele muszą być dostosowane do formy używanej przez nową wersję, przy czym, najlepiej byłoby to zrobić bez utraty danych. Nazywa się to migracją. Tworzenie tabel w pierwotnie pustej bazie to szczególny przypadek migracji. Migrację można zintegrować z samą aplikacją. Zarówno w .NET, jak i w NodeJS istnieją frameworki do migracji. W naszym przypadku, dla bezpieczeństwa, mikroserwisy są pozbawione prawa zmieniania schematu danych, a migracja jest wykonywana przy pomocy Liquibase.
  • Uruchomić Amazon LocalStack. To implementacja usług AWS do uruchamiania lokalnie. Dla LocalStack dostępny jest gotowy obraz na Docker Hub.
  • Uruchomić skrypt do tworzenia niezbędnych encji w LocalStack. Skrypty shellowe wykorzystują AWS CLI.

Do testowania w projekcie używany jest Postman. Był on używany wcześniej, ale uruchamiano go ręcznie i testowano aplikację już wdrożoną na środowisku. To narzędzie umożliwia wykonywanie dowolnych zapytań HTTP(S) i sprawdzanie, czy odpowiedzi spełniają oczekiwania. Zapytania są grupowane w kolekcję, którą można uruchomić całościowo.

Automatyczne testowanie mikroserwisów w Dockerze dla ciągłej integracji

Jak działają automatyczne testy

W trakcie testu w Dockerze działa wszystko: zarówno testowana usługa, jak i Postgres, narzędzie do migracji oraz Postman, a właściwie jego wersja konsolowa – Newman.

Docker rozwiązuje szereg problemów:

  • Niezależność od konfiguracji hosta;
  • Instalacja zależności: docker pobiera obrazy z Docker Hub;
  • Przywrócenie systemu do pierwotnego stanu: wystarczy usunąć kontenery.

Docker-compose łączy kontenery w wirtualną sieć, izolowaną od internetu, w której kontenery znajdują się nawzajem za pomocą nazw domen.

Testem zarządza skrypt shellowy. Do uruchomienia testu na Windowsie używamy git-bash. Dzięki temu wystarcza jeden skrypt zarówno dla Windowsa, jak i dla Linuksa. Git i Docker są zainstalowane u wszystkich deweloperów pracujących nad projektem. Przy instalacji Gita na Windowsie instalowany jest również git-bash, więc również wszyscy go mają.

Skrypt wykonuje następujące kroki:

  • Budowanie obrazów dockera
    docker-compose build
  • Uruchomienie bazy danych i LocalStack
    docker-compose up -d
  • Migracja bazy danych i przygotowanie LocalStack
    docker-compose run
  • Uruchomienie testowanej usługi
    docker-compose up -d
  • Uruchomienie testu (Newman)
  • Zatrzymanie wszystkich kontenerów
    docker-compose down
  • Wysyłanie wyników do Slacka
    Mamy czat, na który trafiają wiadomości z zielonym ptaszkiem lub czerwonym krzyżykiem i linkiem do logu.

W tych krokach używane są następujące obrazy Docker:

  • Testowana usługa – to ten sam obraz, co dla środowiska produkcyjnego. Konfiguracja testowa – za pomocą zmiennych środowiskowych.
  • Dla Postgresa, Redis i LocalStack używamy gotowych obrazów z Docker Hub. Są też gotowe obrazy dla Liquibase i Newman. Budujemy nasze na ich bazie, dodając nasze pliki.
  • Do przygotowania LocalStack używamy gotowego obrazu AWS CLI, na podstawie którego tworzony jest obraz zawierający skrypt.

Używając wolumeny, można nie budować obrazu Docker tylko w celu dodania plików do kontenera. Jednak wolumeny nie nadają się do naszego środowiska, ponieważ zadania Gitlab CI same działają w kontenerach. Z takiego kontenera można zarządzać dockerem, ale wolumeny montują foldery tylko z systemu hosta, a nie z innego kontenera.

Problemy, na jakie można się natknąć

Oczekiwanie na gotowość

Kiedy kontener z usługą jest uruchomiony, to jeszcze nie oznacza, że jest gotowy do przyjmowania połączeń. Trzeba poczekać na połączenie, aby kontynuować.

To zadanie czasem rozwiązują za pomocą skryptu wait-for-it.sh, który czeka na możliwość nawiązania połączenia TCP. Jednak LocalStack może zwrócić błąd 502 Bad Gateway. Ponadto składa się z wielu usług, i jeśli jedna z nich jest gotowa, to nic nie mówi o pozostałych.

Rozwiązanie: skrypty przygotowawcze LocalStack, które oczekują na odpowiedź 200 zarówno od SQS, jak i od SNS.

Konflikty równoległych zadań

Kilka testów może działać jednocześnie na jednym hoście Docker, dlatego nazwy kontenerów i sieci muszą być unikalne. Co więcej, testy z różnych gałęzi tej samej usługi także mogą działać równocześnie, więc samo dodanie nazw w każdym pliku compose nie wystarczy.

Rozwiązanie: skrypt ustawia unikalną wartość zmiennej COMPOSE_PROJECT_NAME.

Cechy Windows

Podczas korzystania z Dockera w systemie Windows jest kilka rzeczy, na które chciałbym zwrócić waszą uwagę, ponieważ to doświadczenie jest ważne dla zrozumienia przyczyn błędów.

  1. Skrypty powłoki w kontenerze muszą mieć linuksowe końce linii.
    Symbol CR dla powłoki to błąd składniowy. Z komunikatu błędu trudno zrozumieć, że to jego wina. Przy edytowaniu takich skryptów w systemie Windows potrzebny jest odpowiedni edytor tekstu. Ponadto system kontroli wersji musi być prawidłowo skonfigurowany.

Oto jak konfiguruje się git:

git config core.autocrlf input

  1. Git-bash emuluje standardowe foldery Linux i przy wywołaniu pliku exe (w tym docker.exe) zamienia absolutne ścieżki Linuxa na ścieżki Windows. Jednak nie ma to sensu dla ścieżek, które nie znajdują się na lokalnej maszynie (lub ścieżek w kontenerze). Takie zachowanie nie jest wyłączane.

Rozwiązanie: dołączać dodatkowy slash na początku ścieżki: \/\/bin zamiast \/bin. Linux rozumie takie ścieżki, dla niego kilka slashów to to samo, co jeden. Ale git-bash takich ścieżek nie rozpoznaje i nie próbuje ich przekształcać.

Wyjście logów

Podczas wykonywania testów chciałbym widzieć logi zarówno od Newmana, jak i od testowanej usługi. Ponieważ zdarzenia tych logów są ze sobą powiązane, ich łączenie w jednej konsoli jest znacznie wygodniejsze niż dwa oddzielne pliki. Newman jest uruchamiany przez docker-compose run, więc jego wyjście trafia do konsoli. Pozostaje sprawić, aby tam również trafiało wyjście usługi.

Początkowe rozwiązanie polegało na tym, aby zrobić docker-compose up bez flagi -d, ale używając możliwości powłoki, wysyłać ten proces w tle:

docker-compose up <service> &

To działało, dopóki nie pojawiła się potrzeba przesyłania logów z dockera do zewnętrznej usługi. docker-compose up przestał wyświetlać logi w konsoli. Jednak polecenie działało docker attach.

Rozwiązanie:

docker attach --no-stdin ${COMPOSE_PROJECT_NAME}__1 &

Konflikt identyfikatorów podczas iteracji testów

Testy są uruchamiane wielokrotnymi iteracjami. Baza nie jest w tym czasie czyszczona. Zapisane w bazie mają unikalne ID. Jeśli zapiszemy konkretne ID w zapytaniach, przy drugiej iteracji wystąpi konflikt.

Aby tego uniknąć, identyfikatory muszą być unikalne, lub należy usuwać wszystkie obiekty utworzone przez test. Usuwanie niektórych obiektów nie jest możliwe, zgodnie z wymaganiami.

Rozwiązanie: generowanie GUID-ów skryptami w Postman.

var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);

Następnie w zapytaniu użyć symbolu {{myUUID}}, który zostanie zastąpiony wartością zmiennej.

Interakcja przez LocalStack

Jeśli testowany serwis odczytuje lub zapisuje do kolejki SQS, to w celu sprawdzenia tego sam test także musi współpracować z tą kolejką.

Rozwiązanie: zapytania z Postman do LocalStack.

API usług AWS jest udokumentowane, co pozwala na wysyłanie zapytań bez SDK.

Jeśli serwis zapisuje do kolejki, to ją odczytujemy i sprawdzamy zawartość wiadomości.

Jeśli serwis wysyła wiadomości do SNS, na etapie przygotowań LocalStack tworzy również kolejkę i subskrybuje ten temat SNS. Dalej wszystko sprowadza się do opisanych wyżej działań.

Jeśli serwis musi odczytać wiadomość z kolejki, to na poprzednim etapie testu piszemy tę wiadomość do kolejki.

Testowanie zapytań HTTP wychodzących z testowanego mikroserwisu

Niektóre serwisy działają przez HTTP z czymś innym niż AWS, a niektóre funkcje AWS nie są implementowane w LocalStack.

Rozwiązanie: w tych przypadkach może pomóc MockServer, który ma gotowy obraz w Docker Hub. Oczekiwane zapytania i odpowiedzi na nie są konfigurowane za pomocą zapytania HTTP. API jest udokumentowane, więc wysyłamy zapytania z Postman.

Testowanie autoryzacji i uwierzytelniania OAuth

Używamy OAuth oraz JSON Web Tokens (JWT). Do testu potrzebny jest dostawca OAuth, którego możemy uruchomić lokalnie.

Cała interakcja serwisu z dostawcą OAuth sprowadza się do dwóch zapytań: najpierw żąda się konfiguracji /.well-known/openid-configuration, a następnie żąda się klucza publicznego (JWKS) pod adresem z konfiguracji. Wszystko to jest zawartością statyczną.

Rozwiązanie: nasz testowy dostawca OAuth to serwer treści statycznej i dwa pliki na nim. Token został wygenerowany raz i zakomitowany w Git.

Cechy testowania SignalR

WebSockety nie działają z Postmanem. Stworzono specjalne narzędzie do testowania SignalR.

Klientem SignalR może być nie tylko przeglądarka. Istnieje biblioteka kliencka dla .NET Core. Klient napisany w .NET Core nawiązuje połączenie, przechodzi autoryzację i oczekuje na określoną sekwencję komunikatów. Jeśli otrzyma niespodziewany komunikat lub połączenie zostaje przerwane, klient kończy działanie z kodem 1. Po otrzymaniu ostatniego oczekiwanego komunikatu kończy działanie z kodem 0.

Równolegle działa Newman. Uruchamiane jest kilka klientów, aby sprawdzić, czy komunikaty docierają do wszystkich, których to dotyczy.

Automatyczne testowanie mikroserwisów w Dockerze dla ciągłej integracji

Do uruchomienia kilku klientów używa się opcji —scale w linii poleceń docker-compose.

Przed uruchomieniem Postmana skrypt czeka na nawiązanie połączenia przez wszystkich klientów.
Problem z oczekiwaniem na połączenie już się pojawił. Tam były serwery, a tutaj klient. Potrzebne jest inne podejście.

Rozwiązanie: klient w kontenerze wykorzystuje mechanizm HealthCheck, aby poinformować skrypt na hoście o swoim statusie. Klient tworzy plik pod określoną ścieżką, na przykład, /healthcheck, gdy tylko połączenie zostanie nawiązane. Skrypt HealthCheck w pliku docker wygląda tak:

HEALTHCHECK --interval=3s CMD if [ ! -e /healthcheck ]; then false; fi

Zespół docker inspect pokazuje dla kontenera standardowy status, status health i kod zakończenia.

Po zakończeniu działania Newmana, skrypt sprawdza, że wszystkie kontenery z klientem zakończyły działanie, i to z kodem 0.

Szczęście jest

Po tym, jak pokonaliśmy opisane powyżej trudności, zyskaliśmy zestaw stabilnych testów. W testach każda usługa działa jako całość, współdziała z bazą danych oraz z Amazon LocalStack.

Te testy chronią zespół ponad 30 deweloperów przed błędami w aplikacji przy skomplikowanej interakcji ponad 10 mikrousług podczas częstych wdrożeń.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster