Z tego artykułu dowiesz się, jak wykorzystałem popularne narzędzia open-source do zbudowania szablonu serverlessowego mikroserwisu, który znacząco usprawnia testowanie.
Na praktycznych przykładach, nauczysz się jak stosować pryncypia architektury heksagonalnej, aby zwiększyć testowalność i dojrzałość kodu. Zobacz, jak możesz wykorzystać wzorce projektowe i inne techniki, z których korzystasz od lat. Nie musisz z nich rezygnować przechodząc na serverless!
Wręcz przeciwnie, bazując na moim doświadczeniu, uważam, że wciąż mają one swoje miejsce w nowoczesnych rozwiązaniach, ponieważ zwiększają czytelność, łatwość utrzymania i testowalność kodu źródłowego.
Jeśli uważasz, że są lepsze sposoby na programowanie niż skrypciarstwo (umieszczanie całości kodu funkcji Lambda w jednym pliku i jednej metodzie) to pokochasz ❤️ to co dla Ciebie przygotowałem.
Brzmi zbyt dobrze, aby było prawdziwe?
Poczekaj, jest więcej! 😎
Zautomatyzowane testy integracyjne i end-to-end (e2e) znacząco usprawniają pracę dewelopera. Wreszcie możesz uwolnić się od cyklu: pisanie kodu -> deployment -> ręczne uruchomienie Lambdy -> sprawdzenia logów -> poprawka błędów -> powrót na początek.
Ale to nie wszystko!
NAJLEPSZA CZĘŚĆ: ten szablon jest dostępny na GitHubie zupełnie za darmo! 😃
Możesz z niego skorzystać już teraz!
Zanim wyjaśnię rozwiązanie, pozwól, że podsumuję typowe problemy, które doprowadziły mnie do stworzenia tego szablonu.
Złudna prostota funkcji lambda
Każdy programista stawiając swoje pierwsze kroki w serverless ma już duży bagaż doświadczeń zdobytych na wcześniejszych projektach. Najczęściej oznacza to, że tworzył aplikacji w architekturze monolitów w ciężkich językach (Java czy C#). Oczywiście, część osób ma już duże doświadczenie w mikroserwisach, ale to nadal większe komponenty niż funkcje.
Przeskakując na serverless pisany w JavaScript czy Python łatwo zachłysnąć się wolnością oferowaną przez te technologie. Funkcje są na tyle małe i proste, a brak (wymuszania użycia) typów sprawiają razem, że wydaje się iż nie trzeba pamiętać o dobrych praktykach rzemiosła programistycznego. Nie ma nic złego w eksperymentowaniu i bawieniu się. Niestety, zbyt często spotykam się z osobami, które zastosowały skrypciarskie podejście w systemach produkcyjnych, a teraz cierpią z powodu słabej utrzymywalności i braku testów.
Bardzo kuszące jest zaimplementowanie funkcji Lambda w zaledwie kilku linijkach. Niestety, na dłuższą metę to się nie opłaca.
Brak testów
Bezpośrednim efektem skrypciarskiej implementacji jest słaba testowalność. Monolityczny kod jest naprawdę trudny do przetestowania, więc ludzie nie piszą żadnych testów. Po prostu tak jest. Konsekwencje braku testów są raczej oczywiste dla doświadczonych programistów, więc nie będę się tutaj rozpisywał na ten temat.
Jednakże, niektórzy ludzie testują swoje aplikacje serverless. Piszą zautomatyzowane testy jednostkowe dla logiki biznesowej lub kodu operującego na usługach AWS używając mocków.
O ile mocki nie są złe (sam ich używam), to trzeba wiedzieć, kiedy powinno się stosować tę technikę. I co ważniejsze, kiedy nie 😉
Mockowanie wszystkich usług AWS nie da Ci żadnej gwarancji, że Twój kod będzie działał po wdrożeniu do chmury. Mocki dają Ci fałszywe poczucie pewności działania. To samo tyczy się localstack i podobnych narzędzi, które emulują AWS w kontenerach.
Dlaczego testujemy?
Zastanawiałeś się może dlaczego w ogóle testujemy? W mojej opinii z dwóch powodów:
- aby mieć zaufanie, że nasz kod działa tak, jak nam się wydaje
- aby uchronić się przed błędami regresji po wprowadzeniu nowych zmian
Zautomatyzowany zestaw testów (test suite) da nam natychmiastową informację zwrotną, że coś jest nie tak z naszym kodem.
Jego brak zmusza do manualnego testowania po każdej zmianie. Można też być odważnym i wdrożyć nowy kod prosto na prod
. Nie no - ja tylko żartowałem 🤣
Nie mam nic przeciwko manualnym testom, ale nie skalują się, wymagają znajomości systemu (np. nowa osoba nie będzie wiedziała jak i co testować), i są powolne. Ponadto, nie można ich wyegzekwować. Mam na myśli to, że nie można uruchomić testów manualnych w pipeline CI/CD.
Jest jeszcze jedna irytująca rzecz. Zbyt często w projektach bez testów lub z kiepskimi testami słyszę, jak moi koledzy mówią “…ale to działało lokalnie na mojej maszynie”. Na prawdę, nic mnie to nie obchodzi! 😤
Jako programista i osoba, która bierze odpowiedzialność za dostarczenie działającego rozwiązania, które jest wolne od błędów, muszę pisać kod łatwy do przetestowania i utrzymania. I mieć pewność, że działa on na prod
w chmurze, a nie na czyimś laptopie.
Rozwiązanie: Jak testować serverless?
Aby rozwiązać powyższe problemy, przygotowałem szablon projektu dla Serverless Framework, który stosuje zasady heksagonalnej architektury w świecie serverless.
Projekt szablonu został stworzony aby osiągnąć dwa cele: usprawniony flow pracy programisty oraz łatwe testowanie, ponieważ obie te rzeczy nie są jeszcze powszechne w świecie serverless.
Mój szablon dostępny jest na GitHubie pod nazwą serverless-hexagonal-template.
Jak z niego korzystać?
Aby zacząć go używać, należy stworzyć nowy projekt na podstawie tego szablonu:
1 | sls create --template-url https://github.com/serverlesspolska/serverless-hexagonal-template \ |
Jeśli nie masz Serverless Framework, zainstaluj go: npm i -g serverless
. Powyższa komenda stworzy Twój nowy projekt na bazie szablonu. Więcej informacji w dokumentacji na GitHubie.
Podejście do testowania
Cała konfiguracje boilerplate, w szczególności frameworka testowego jest
, pluginów oraz innych narzędzi open-source jest zawarta w szablonie. Nowy projekt jest od razu gotowy do działania.
Szablon zawiera dwie przykładowe funkcje Lambda oraz zbiór:
- testów jednostkowych (unit tests)
- testów integracyjnych
- testów end-to-end (e2e).
Podział ten został wprowadzony, ponieważ różne rodzaje testów spełniają różne potrzeby.
Testy jednostkowe są wykonywane lokalnie (na komputerze dewelopera lub serwerze CI/CD) i nie wymagają dostępu do żadnych zasobów w chmurze AWS lub w Internecie.
Natomiast testy integracyjne oraz e2e wymagają rzeczywistych usług AWS wdrożonych w chmurze. Dlatego przed ich rozpoczęciem należy zdeployować projekt wykonując sls deploy
.
Testy integracyjne
Teraz wykonaj komendę npm run integration
, aby uruchomić testy integracyjne.
Specjalny plugin Serverless Framework (serverless-export-env) połączy się z Twoim kontem AWS i zapisze lokalnie w pliku .awsenv
wszystkie zmienne środowiskowe wszystkich funkcji Lambda zdefiniowanych w projekcie.
1 | stage=dev |
Następnie, wartości z tego pliku są wstrzykiwane do kontekstu testowego jest
. Oznacza to, że ilekroć Twój kod będzie odwoływał się do zmiennej środowiskowej np. process.env.MY_ENV_VAR
w czasie wykonywania testu, otrzyma dokładnie taką samą wartość jak gdyby był uruchamiany w chmurze.
W ten sposób kod aplikacji (mikroserwisu) może być testowany lokalnie równocześnie używając prawdziwych zasobów w chmurze. Najlepszą rzeczą jest to, że pisząc czysty kod w myśl architektury heksagonalnej, kod implementacji nie ma świadomości kontekstu testowego. Nie musisz do niego dodawać żadnych specjalnych rzeczy, aby dało radę go przetestować (to by było brzydkie, nieprawdaż?)
Testy automatyczne jest
są wykonywane lokalnie. Testują one twoje lokalne pliki z zasobami w chmurze. Na przykład, w serverless-hexagonal-template, zaimplementowałem testy, które używają tabeli DynamoDB w chmurze. Źródła testów znajdziesz tu i tu.
Kolejny z testów (źródła) skupia się na integracji AWS API Gateway i funkcji Lambda. Jest to o tyle istotne, że rozwiązania serverless mocno zależą od wielu zasobów w chmurze. Wiele błędów wynika z niewłaściwej konfiguracji. Posiadanie takich testów integracyjnych pozwala na dokładne sprawdzenie tego obszaru.
1 | const { default: axios } = require('axios') |
Problemy z integracją i konfiguracją usług są głównym motorem zmian dotyczących tego, jak branża patrzy na testowanie.
Właśnie dlatego tak duży nacisk kładę na testy integracyjne, gdyż są one po prostu ważniejsze w aplikacjach serverless.
Szczerze mówiąc, nie tylko w serverless. W każdym systemie rozproszonym testy jednostkowe to po prostu za mało.
Testy end-to-end (e2e)
Czasami testy integracyjne nie wystarczą, ponieważ musimy przetestować cały łańcuch komunikacji pomiędzy zestawem komponentów.
Przykładem takiego testu może być żądanie POST
wysłane do endpointu AWS API Gateway /item
i sprawdzenie czy funkcja Lambda processItem
została wywołana przez DynamoDB Streams w wyniku zapisania nowego elementu przez funkcję Lambda createItem
wywołaną przez żądanie. Takie podejście testuje łańcuch zdarzeń, które dzieją się w chmurze i daje pewność, że integracja pomiędzy wieloma usługami jest dobrze skonfigurowana.
Wspomniane łańcuchy zdarzeń to oczywiście nic innego jak Architektura Sterowana Zdarzeniami (Event Driven Architecture) w praktyce. To właśnie one stanowią o sile podejścia cloud-native i równocześnie powodują, że korzystanie z rozwiązań typu localstack jest ryzykowne (brak jakiejkolwiek gwarancji, że te integracja działają lokalnie tak samo jak w AWS).
Przy okazji, testy e2e wspaniale się sprawdzają, gdy chcemy przetestować kod, który łączy się do baz RDS, do których nie ma dostępu spoza VPC.
Architektura heksagonalna
W naturalny sposób wprowadza ład do naszego kodu, gdyż podział na niezależne moduły staje się intuicyjny. Pozwala na lepszą separację problemów i ułatwia pisanie kodu zgodnego z zasadą SRP (Single Responsibility Principle). Są to kluczowe cechy architektury łatwej w utrzymaniu, rozbudowie i testowaniu.
Wybór tego konkretnego stylu architektonicznego łączy się z zaproponowaną strukturą katalogów projektu i konwencją nazewniczą. Więcej na ten temat można przeczytać w dokumentacji.
Dość powiedzieć, że definiuje ona gdzie co powinno być umieszczone (np. kod źródłowy w folderze src/
, testy w __tests__/
itd.), dzięki czemu nie trzeba tracić czasu na zastanawianie się nad tym, za każdym razem, gdy rozpoczyna się nowy projekt. Dodatkowo, konwencja tworzy wspólny język dla członków zespołu. W ten sposób zmniejsza się przeciążenie poznawcze programistów podczas przełączania się pomiędzy projektami rozpoczętymi z tego szablonu.
Jak stworzyłem szablon?
Szablon został wypracowany w wyniku wieloletniej pracy z funkcjami Lambda z wykorzystaniem Serverless Framework. Czerpie też ze zbiorowego doświadczenia społeczności (której jestem wdzięczny) ucieleśnionego w książkach, wystąpieniach, nagraniach i artykułach.
Osobiście miałem dość kiepskiego, nieefektywnego workflow programisty serverless:
- pisanie kodu
- deployment
- ręczne uruchomienie Lambdy
- sprawdzenia logów
- poprawka błędów
- powrót na początek.
Znasz to skądś?
Postanowiłem, że chcę naprawić ten problem. Skupiłem się na testowaniu, ponieważ wiedziałem, że jego rozwiązanie pozwoli mi pracować w dużo bardziej dojrzały sposób. Wiele lat temu byłem programistą Java i wiedziałem, że flow programisty może być znacznie lepszy. 😃
Spędziłem wiele wieczorów czytając o testowaniu serverless i eksperymentując. Na szczęście już od jakiegoś czasu korzystałem z heksagonalnej architektury, więc łatwo było mi myśleć o testowaniu w kontekście pojedynczych modułów, a nie całych funkcji Lambda. W końcu znalazłem kilka artykułów na temat wtyczki serverless-export-env, która okazał się być brakującym ogniwem, które pozwoliło mi w łatwy, zautomatyzowany sposób dopiąć wszystko razem. To było dla mnie najważniejsze. Wiedziałem, że proces ten musi być prosty i w pełni generyczny, tak abym mógł go wykorzystać w każdym projekcie.
Kiedy zacząłem stosować to podejście, od razu zauważyłem, jak bardzo poprawił się mój workflow programisty. Wreszcie mogłem wprowadzać zmiany w locie!
Byłem w stanie napisać od 70 do 90 procent kodu bez ciągłych re-deploymentów. To była ogromna poprawa! W niektórych przypadkach używałem TDD (Test Driven Development), które można stosować bez problemu w takim podejściu.
Po wdrożeniu kilku mikroserwisów (projektów) miałem pewność, że ta metoda się sprawdza. Postanowiłem, że chcę się podzielić tym podejściem ze społecznością. Uwielbiam pomagać ludziom na całym świecie budować systemy przy użyciu serverless. Pomagać im uczyć się i stawać się lepszymi programistami. To była logiczna rzecz do zrobienia. Nie chcę tej wiedzy trzymać tylko dla siebie 😎
Jednak zamiast pisać zwykły artykuł, postanowiłem stworzyć szablon Serverless Framework, który zawiera wszystkie rzeczy i praktyki, które znałem, gotowe do użycia natychmiast. Dzięki temu, każdy może z nich skorzystać.
Dlaczego powinieneś użyć tego szablonu?
W skrócie, użycie serverless-hexagonal-template da Ci:
- Gotowy do wdrożenia na produkcji (production-ready) szablon mikroserwisu serverless
- Większe zaufanie do swojego rozwiązania (testy!)
- Efektywny i powtarzalny workflow programisty
- Dobrze przemyślaną strukturę projektu
- Zwiększoną reużywalność kodu
- Czysty kod i dojrzały design - wykorzystanie wzorców i dobrych praktyk, których nauczyłeś się przez lata
- Możliwość testowania w pipelinach CI/CD.
Ponadto:
- Koniec z ciągłymi re-deploymentami w celu testowania kodu.
- Koniec z ręcznym testowaniem
- Koniec ze skrypciarstwem
- Koniec z błędami regresji
- Koniec z wymówkami typu to działało na moim komputerze 😉
Przetestowane na własnej skórze™ ;-)
Moja przygoda z serverless trwa od 2016 roku. Zanim zacząłem stosować opisane tutaj podejście miałem wiele projektów z testami jednostkowymi lub bez testów w ogóle. Ciężko było dodawać do nich nowe funkcjonalności bez wprowadzania nowych bugów (błędów regresji) lub przynajmniej obawy takiej ewentualności. Testy jednostkowe po prostu nie wystarczały, a ja nie wiedziałem jak napisać poprawne testy na moje błędy. Każda zmiana musiała być zdeployowana i ręcznie przetestowana.
Obecnie, wdrażanie i modyfikowanie projektów to zupełnie inna historia. Dodanie testów integracyjnych i e2e pozwoliło mi komfortowo wprowadzać zmiany bez obaw o błędy regresji. Mojej pracy nie przerywają już ciągłe re-deploymenty projektów do chmury. Oczywiście, nadal są one potrzebne, ale prawie wszystko można przetestować od razu po pierwszym deploymencie i poprawnym zdefiniowaniu zmiennych środowiskowych.
Podsumowując, to podejście oszczędza to sporo czasu i ułatwia życie dewelopera.
Try it out!
Więc, jeśli chcesz mieć super workflow programisty i dobrze przetestowany system spróbuj tego rozwiązania. Zajmie Ci to nie więcej niż 5 minut.
- Stwórz swój projekt z szablonu
sls create --template-url https://github.com/serverlesspolska/serverless-hexagonal-template --name <nazwa twojego projektu>
. - Zainstaluj zależności
npm i
. - Wykonaj testy jednostkowe
npm run test
. - Zrób wdrożenie do chmury
sls deploy
. - Uruchom testy integracyjne
npm run integration
- Uruchom testy end-to-end
npm run e2e
.
Następnie przeanalizuj mój kod i sprawdź jak pisać testy do aplikacji serverless. Zacznij używać tego szablonu w swoich projektach, a następnie daj mi gwiazdkę ⭐️ na GitHubie: serverless-hexagonal-template. Powodzenia i miłego testowania!
Chcesz zobaczyć wideo o tym, jak użyć szablonu?
Temat testowania Cię wciągnął i masz ochotę na więcej. Świetnie. Mam coś dla Ciebie. 🎁
Specjalna seria wiedzy w 5-ciu mailach.