Zautomatyzowane testowanie mikroserwisów w Dockerze w celu ciągłej integracji

W projektach związanych z rozwojem architektury mikroserwisowej CI/CD przechodzi z kategorii przyjemnej okazji do kategorii pilnej konieczności. Automatyczne testowanie jest integralną częścią ciągłej integracji, a kompetentne podejście może zapewnić zespołowi wiele przyjemnych wieczorów w gronie rodziny i przyjaciół. W przeciwnym razie istnieje ryzyko, że projekt nigdy nie zostanie ukończony.

Możliwe jest pokrycie całego kodu mikroserwisu testami jednostkowymi z próbnymi obiektami, ale to tylko częściowo rozwiązuje problem i pozostawia wiele pytań i trudności, szczególnie podczas testowania pracy z danymi. Jak zwykle najpilniejsze są testowanie spójności danych w relacyjnej bazie danych, testowanie pracy z usługami w chmurze oraz przyjęcie błędnych założeń podczas pisania mock obiektów.

Wszystko to i trochę więcej można rozwiązać testując cały mikroserwis w kontenerze Docker. Niewątpliwą zaletą zapewniającą ważność testów jest to, że testowane są te same obrazy Dockera, które trafiają do produkcji.

Automatyzacja tego podejścia stwarza szereg problemów, których rozwiązanie zostanie opisane poniżej:

  • konflikty równoległych zadań w tym samym hoście dokującym;
  • konflikty identyfikatorów w bazie danych podczas iteracji testów;
  • oczekiwanie, aż mikroserwisy będą gotowe;
  • łączenie i wysyłanie logów do systemów zewnętrznych;
  • testowanie wychodzących żądań HTTP;
  • testowanie gniazd internetowych (przy użyciu SignalR);
  • testowanie uwierzytelniania i autoryzacji OAuth.

Ten artykuł jest oparty na moje przemówienie na SECR 2019. Dla tych, którzy są zbyt leniwi, aby czytać, oto nagranie przemówienia.

Zautomatyzowane testowanie mikroserwisów w Dockerze w celu ciągłej integracji

W tym artykule opowiem jak za pomocą skryptu uruchomić testowaną usługę, bazę danych i usługi Amazon AWS w Dockerze, następnie przeprowadzić testy na Postmanie i po ich zakończeniu zatrzymać i usunąć utworzone kontenery. Testy są wykonywane przy każdej zmianie kodu. W ten sposób mamy pewność, że każda wersja poprawnie współpracuje z bazą danych i usługami AWS.

Ten sam skrypt jest uruchamiany zarówno przez samych programistów na komputerach stacjonarnych z systemem Windows, jak i przez serwer Gitlab CI pod Linuksem.

Aby było to uzasadnione, wprowadzenie nowych testów nie powinno wymagać instalacji dodatkowych narzędzi ani na komputerze programisty, ani na serwerze, na którym uruchamiane są testy na zatwierdzeniu. Docker rozwiązuje ten problem.

Test musi zostać uruchomiony na serwerze lokalnym z następujących powodów:

  • Sieć nigdy nie jest całkowicie niezawodna. Spośród tysiąca próśb jedno może się nie powieść;
    W takim przypadku test automatyczny nie zadziała, praca zostanie zatrzymana i będziesz musiał szukać przyczyny w logach;
  • Niektóre usługi stron trzecich nie zezwalają na zbyt częste żądania.

Ponadto korzystanie ze stojaka jest niepożądane, ponieważ:

  • Stanowisko może zostać zepsute nie tylko przez działający na nim zły kod, ale także przez dane, których prawidłowy kod nie jest w stanie przetworzyć;
  • Bez względu na to, jak bardzo staramy się przywrócić wszystkie zmiany wprowadzone przez test podczas samego testu, coś może pójść nie tak (w przeciwnym razie po co testować?).

O organizacji projektu i procesu

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

W projekcie zastosowano standardowy proces ciągłej integracji, który obejmuje testowanie mikroserwisu przy każdym zatwierdzeniu. Po przydzieleniu zadania programista wprowadza zmiany w mikroserwisie, testuje go ręcznie i uruchamia wszystkie dostępne testy automatyczne. W razie potrzeby programista zmienia testy. Jeśli nie zostaną znalezione żadne problemy, zostanie dokonane zatwierdzenie gałęzi tego problemu. Po każdym zatwierdzeniu na serwerze automatycznie uruchamiane są testy. Połączenie we wspólną gałąź i uruchomienie na niej automatycznych testów następuje po pomyślnym przeglądzie. Jeśli testy na gałęzi współdzielonej zakończą się pomyślnie, usługa zostanie automatycznie zaktualizowana w środowisku testowym w usłudze Amazon Elastic Container Service (bench). Podstawka jest niezbędna wszystkim programistom i testerom i nie zaleca się jej łamania. Testerzy w tym środowisku sprawdzają poprawkę lub nową funkcję, wykonując testy ręczne.

Architektura projektu

Zautomatyzowane testowanie mikroserwisów w Dockerze w celu ciągłej integracji

Aplikacja składa się z kilkunastu usług. Część z nich jest napisana w .NET Core, a część w NodeJ. Każda usługa działa w kontenerze Docker w usłudze Amazon Elastic Container Service. Każdy ma własną bazę danych Postgres, a niektóre mają także Redis. Nie ma wspólnych baz danych. Jeśli kilka usług potrzebuje tych samych danych, wówczas dane te w przypadku ich zmiany są przesyłane do każdej z tych usług za pośrednictwem SNS (Simple Notification Service) i SQS (Amazon Simple Queue Service), a usługi zapisują je we własnych, odrębnych bazach danych.

SQS i SNS

SQS umożliwia umieszczanie wiadomości w kolejce i odczytywanie wiadomości z kolejki za pomocą protokołu HTTPS.

Jeśli z jednej kolejki odczytuje kilka usług, wówczas każda wiadomość dociera tylko do jednej z nich. Jest to przydatne w przypadku uruchamiania kilku wystąpień tej samej usługi w celu rozłożenia obciążenia między nimi.

Jeśli chcesz, aby każda wiadomość była dostarczana do wielu usług, każdy odbiorca musi mieć własną kolejkę, a SNS jest potrzebny do duplikowania wiadomości w wielu kolejkach.

W SNS tworzysz temat i subskrybujesz go np. kolejkę SQS. Możesz wysyłać wiadomości do tematu. W takim przypadku wiadomość jest wysyłana do każdej kolejki subskrybowanej w tym temacie. SNS nie ma metody czytania wiadomości. Jeśli podczas debugowania lub testowania chcesz dowiedzieć się, co jest wysyłane do SNS, możesz utworzyć kolejkę SQS, zasubskrybować ją do żądanego tematu i przeczytać kolejkę.

Zautomatyzowane testowanie mikroserwisów w Dockerze w celu ciągłej integracji

API Gateway

Większość usług nie jest bezpośrednio dostępna z Internetu. Dostęp odbywa się poprzez API Gateway, który sprawdza uprawnienia dostępu. To także nasza usługa i dla niej też są testy.

Powiadomienia w czasie rzeczywistym

Aplikacja korzysta SygnałRaby wyświetlać użytkownikowi powiadomienia w czasie rzeczywistym. Jest to zaimplementowane w usłudze powiadomień. Jest dostępny bezpośrednio z Internetu i sam współpracuje z OAuth, ponieważ wbudowanie obsługi gniazd sieciowych w Gateway okazało się niepraktyczne w porównaniu z integracją OAuth i usługi powiadomień.

Dobrze znane podejście do testowania

Testy jednostkowe zastępują takie rzeczy jak baza danych obiektami próbnymi. Jeśli na przykład mikrousługa próbuje utworzyć rekord w tabeli z kluczem obcym, a rekord, do którego odwołuje się ten klucz, nie istnieje, wówczas żądanie nie może zostać zrealizowane. Testy jednostkowe nie mogą tego wykryć.

В artykuł od Microsoftu Proponuje się wykorzystanie bazy danych w pamięci i implementację obiektów próbnych.

Baza danych w pamięci jest jednym z systemów DBMS obsługiwanych przez Entity Framework. Został stworzony specjalnie do testów. Dane w takiej bazie danych przechowywane są jedynie do czasu zakończenia procesu korzystającego z nich. Nie wymaga tworzenia tabel i nie sprawdza integralności danych.

Obiekty próbne modelują klasę, którą zastępują, tylko w takim stopniu, w jakim twórca testu rozumie, jak to działa.

W artykule firmy Microsoft nie określono, jak zmusić Postgres do automatycznego uruchamiania i przeprowadzania migracji po uruchomieniu testu. Moje rozwiązanie to robi, a ponadto nie dodaje do samego mikroserwisu żadnego kodu specjalnie do testów.

Przejdźmy do rozwiązania

Podczas procesu tworzenia oprogramowania stało się jasne, że testy jednostkowe nie wystarczą do znalezienia wszystkich problemów w odpowiednim czasie, dlatego zdecydowano się podejść do tego problemu z innej perspektywy.

Konfigurowanie środowiska testowego

Pierwszym zadaniem jest wdrożenie środowiska testowego. Kroki wymagane do uruchomienia mikroserwisu:

  • Skonfiguruj testowaną usługę dla środowiska lokalnego, w zmiennych środowiskowych podaj szczegóły połączenia z bazą danych i AWS;
  • Uruchom Postgres i wykonaj migrację, uruchamiając Liquibase.
    W relacyjnych systemach DBMS przed zapisaniem danych do bazy danych należy utworzyć schemat danych, czyli inaczej tabele. Podczas aktualizacji aplikacji tabele należy doprowadzić do postaci używanej w nowej wersji i najlepiej bez utraty danych. Nazywa się to migracją. Tworzenie tabel w początkowo pustej bazie danych jest szczególnym przypadkiem migracji. Migrację można wbudować w samą aplikację. Zarówno .NET, jak i NodeJS mają platformy migracji. W naszym przypadku ze względów bezpieczeństwa mikroserwisy pozbawione są prawa do zmiany schematu danych, a migracja odbywa się za pomocą Liquibase.
  • Uruchom Amazon LocalStack. Jest to implementacja usług AWS do uruchamiania w domu. Istnieje gotowy obraz dla LocalStack na Docker Hub.
  • Uruchom skrypt, aby utworzyć niezbędne encje w LocalStack. Skrypty powłoki korzystają z interfejsu CLI AWS.

Używany do testów w projekcie Listonosz. Istniał już wcześniej, ale został uruchomiony ręcznie i przetestował aplikację już wdrożoną na stoisku. Narzędzie to umożliwia wysyłanie dowolnych żądań HTTP(S) i sprawdzanie, czy odpowiedzi odpowiadają oczekiwaniom. Zapytania są łączone w kolekcję i można uruchomić całą kolekcję.

Zautomatyzowane testowanie mikroserwisów w Dockerze w celu ciągłej integracji

Jak działa test automatyczny?

Podczas testu w Dockerze wszystko działa: testowana usługa, Postgres, narzędzie do migracji oraz Postman, a właściwie jego konsolowa wersja – Newman.

Docker rozwiązuje szereg problemów:

  • Niezależność od konfiguracji hosta;
  • Instalowanie zależności: Docker pobiera obrazy z Docker Hub;
  • Przywrócenie systemu do pierwotnego stanu: po prostu usunięcie pojemników.

komponowanie dokera łączy kontenery w odizolowaną od Internetu sieć wirtualną, w której kontenery odnajdują się nawzajem według nazw domen.

Test jest kontrolowany przez skrypt powłoki. Aby uruchomić test w systemie Windows, używamy git-bash. Zatem jeden skrypt wystarczy zarówno dla systemu Windows, jak i Linux. Git i Docker są instalowane przez wszystkich programistów projektu. Podczas instalacji Gita w systemie Windows instalowany jest git-bash, więc każdy też to ma.

Skrypt wykonuje następujące kroki:

  • Tworzenie obrazów dokowanych
    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 <сервис>
  • Uruchamianie testu (Newman)
  • Zatrzymanie wszystkich kontenerów
    docker-compose down
  • Publikowanie wyników w Slacku
    Mamy czat, na który trafiają wiadomości z zielonym znacznikiem lub czerwonym krzyżykiem i linkiem do dziennika.

W tych krokach biorą udział następujące obrazy platformy Docker:

  • Testowana usługa ma taki sam obraz jak w przypadku produkcji. Konfiguracja testu odbywa się za pomocą zmiennych środowiskowych.
  • W przypadku Postgres, Redis i LocalStack wykorzystywane są gotowe obrazy z Docker Hub. Istnieją również gotowe obrazy dla Liquibase i Newmana. Budujemy nasz na ich szkielecie, dodając tam nasze pliki.
  • Aby przygotować LocalStack, korzystasz z gotowego obrazu AWS CLI i tworzysz obraz zawierający oparty na nim skrypt.

Korzystanie z kłęby, nie musisz budować obrazu Dockera, aby dodać pliki do kontenera. Jednak woluminy nie są odpowiednie dla naszego środowiska, ponieważ same zadania Gitlab CI działają w kontenerach. Możesz sterować Dockerem z takiego kontenera, ale woluminy montują tylko foldery z systemu hosta, a nie z innego kontenera.

Problemy, które możesz napotkać

Czekam na gotowość

To, że kontener z usługą jest uruchomiony, nie oznacza, że ​​jest gotowy do przyjmowania połączeń. Musisz poczekać, aż połączenie będzie kontynuowane.

Czasami problem ten rozwiązuje się za pomocą skryptu czekaj na to.sh, który czeka na możliwość nawiązania połączenia TCP. Jednak LocalStack może zgłosić błąd 502 Bad Gateway. Ponadto składa się z wielu usług i jeśli jedna z nich jest gotowa, nie mówi to nic o pozostałych.

decyzja: Skrypty udostępniające LocalStack, które czekają na 200 odpowiedzi zarówno z SQS, jak i SNS.

Równoległe konflikty zadań

Na tym samym hoście Dockera można uruchomić wiele testów jednocześnie, dlatego nazwy kontenerów i sieci muszą być unikalne. Co więcej, testy z różnych gałęzi tej samej usługi mogą być uruchamiane jednocześnie, dlatego nie wystarczy wpisanie ich nazw w każdym pliku tworzenia.

decyzja: Skrypt ustawia zmienną COMPOSE_PROJECT_NAME na unikalną wartość.

Cechy okien

Jest wiele rzeczy, na które chcę zwrócić uwagę podczas korzystania z Dockera w systemie Windows, ponieważ te doświadczenia są ważne dla zrozumienia przyczyn występowania błędów.

  1. Skrypty powłoki w kontenerze muszą mieć końcówki linii systemu Linux.
    Symbol powłoki CR jest błędem składniowym. Na podstawie komunikatu o błędzie trudno stwierdzić, czy tak jest. Do edycji takich skryptów w systemie Windows potrzebny jest odpowiedni edytor tekstu. Ponadto system kontroli wersji musi być odpowiednio skonfigurowany.

Tak jest skonfigurowany git:

git config core.autocrlf input

  1. Git-bash emuluje standardowe foldery Linuksa i podczas wywoływania pliku exe (w tym docker.exe) zastępuje bezwzględne ścieżki Linuksa ścieżkami Windows. Nie ma to jednak sensu w przypadku ścieżek, które nie znajdują się na komputerze lokalnym (lub ścieżek w kontenerze). Tego zachowania nie można wyłączyć.

decyzja: dodaj dodatkowy ukośnik na początku ścieżki: //bin zamiast /bin. Linux rozumie takie ścieżki; dla niego kilka ukośników jest tym samym. Ale git-bash nie rozpoznaje takich ścieżek i nie próbuje ich konwertować.

Dane wyjściowe dziennika

Podczas uruchamiania testów chciałbym zobaczyć logi zarówno Newmana, jak i testowanej usługi. Ponieważ zdarzenia tych logów są ze sobą powiązane, połączenie ich w jednej konsoli jest znacznie wygodniejsze niż dwa osobne pliki. Newman uruchamia poprzez uruchomienie tworzenia dokera, a zatem jego dane wyjściowe trafiają do konsoli. Pozostaje tylko upewnić się, że dane wyjściowe usługi również tam trafią.

Oryginalne rozwiązanie było zrobić dokuj-skomponuj bez flagi -d, ale korzystając z możliwości powłoki, wyślij ten proces do tła:

docker-compose up <service> &

Działało to do czasu, gdy konieczne było wysyłanie dzienników z Dockera do usługi strony trzeciej. dokuj-skomponuj przestał wysyłać logi do konsoli. Zespół jednak działał dokowanie dokuje.

decyzja:

docker attach --no-stdin ${COMPOSE_PROJECT_NAME}_<сервис>_1 &

Konflikt identyfikatorów podczas iteracji testu

Testy przeprowadzane są w kilku iteracjach. Baza danych nie została wyczyszczona. Rekordy w bazie posiadają unikalne identyfikatory. Jeśli w żądaniach zapiszemy konkretne identyfikatory, już w drugiej iteracji otrzymamy konflikt.

Aby tego uniknąć, albo identyfikatory muszą być unikalne, albo wszystkie obiekty utworzone w wyniku testu muszą zostać usunięte. Niektórych obiektów nie można usunąć ze względu na wymagania.

decyzja: generuj identyfikatory GUID za pomocą skryptów Postmana.

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

Następnie użyj symbolu w zapytaniu {{myUUID}}, która zostanie zastąpiona wartością zmiennej.

Współpraca poprzez LocalStack

Jeśli testowana usługa czyta lub zapisuje do kolejki SQS, to aby to zweryfikować, sam test musi również działać z tą kolejką.

decyzja: żądania od Postmana do LocalStack.

Interfejs API usług AWS jest udokumentowany, co pozwala na wykonywanie zapytań bez pakietu SDK.

Jeśli usługa zapisuje do kolejki, to czytamy ją i sprawdzamy zawartość wiadomości.

Jeśli usługa wysyła wiadomości do SNS, na etapie przygotowania LocalStack również tworzy kolejkę i subskrybuje ten temat SNS. Potem wszystko sprowadza się do tego, co opisano powyżej.

Jeśli usługa potrzebuje odczytać wiadomość z kolejki, to w poprzednim kroku testowym zapisujemy tę wiadomość do kolejki.

Testowanie żądań HTTP pochodzących z testowanej mikrousługi

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

decyzja: w takich przypadkach może pomóc MockServer, w którym znajduje się gotowy obraz Centrum Dockera. Oczekiwane żądania i odpowiedzi na nie konfiguruje się za pomocą żądania HTTP. API jest udokumentowane, dlatego wysyłamy prośby do Postmana.

Testowanie uwierzytelniania i autoryzacji OAuth

Używamy OAuth i Tokeny internetowe JSON (JWT). Test wymaga dostawcy OAuth, którego możemy uruchomić lokalnie.

Cała interakcja pomiędzy usługą a dostawcą OAuth sprowadza się do dwóch żądań: po pierwsze, żądana jest konfiguracja /.well-known/openid-configuration, a następnie żądany jest klucz publiczny (JWKS) pod adresem z konfiguracji. Wszystko to jest treścią statyczną.

decyzja: Nasz testowy dostawca OAuth to serwer zawartości statycznej i znajdujące się na nim dwa pliki. Token jest generowany raz i przekazywany do Git.

Funkcje testowania sygnalizującego

Postman nie współpracuje z websocketami. Do testowania SignalR-a stworzono specjalne narzędzie.

Klient sygnalizujący może być czymś więcej niż tylko przeglądarką. W platformie .NET Core dostępna jest biblioteka kliencka. Klient napisany w .NET Core nawiązuje połączenie, zostaje uwierzytelniony i czeka na określoną sekwencję komunikatów. Jeśli otrzyma nieoczekiwaną wiadomość lub połączenie zostanie utracone, klient wyjdzie z kodem 1. Jeśli otrzyma ostatnią oczekiwaną wiadomość, klient wyjdzie z kodem 0.

Newman pracuje jednocześnie z klientem. Uruchomiono kilku klientów, którzy sprawdzają, czy wiadomości docierają do wszystkich, którzy ich potrzebują.

Zautomatyzowane testowanie mikroserwisów w Dockerze w celu ciągłej integracji

Aby uruchomić wielu klientów, użyj opcji --skala w wierszu poleceń docker-compose.

Przed uruchomieniem skrypt Postman czeka, aż wszyscy klienci nawiążą połączenie.
Spotkaliśmy się już z problemem oczekiwania na połączenie. Ale były serwery, a tu jest klient. Potrzebne jest inne podejście.

decyzja: klient w kontenerze korzysta z mechanizmu Kontrola zdrowiaaby poinformować skrypt na hoście o jego statusie. Klient tworzy plik w określonej ścieżce, powiedzmy /healthcheck, zaraz po nawiązaniu połączenia. Skrypt HealthCheck w pliku dokowanym wygląda następująco:

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

Zespół Inspekcja dokera Pokazuje stan normalny, stan kondycji i kod wyjścia kontenera.

Po zakończeniu działania Newmana skrypt sprawdza, czy wszystkie kontenery z klientem zostały zakończone, podając kod 0.

Szczęście istnieje

Po pokonaniu opisanych powyżej trudności mieliśmy zestaw testów stabilnego działania. W testach każda usługa działa jako pojedyncza jednostka, współdziałając z bazą danych i Amazon LocalStack.

Testy te chronią zespół ponad 30 programistów przed błędami w aplikacji przy złożonej interakcji ponad 10 mikrousług z częstymi wdrożeniami.

Źródło: www.habr.com

Dodaj komentarz