Kategoria: Uncategorized

  • Spring Boot vs Quarkus – czy stare wygrywa z szybkim?

    „Czemu jeszcze nie przepisaliśmy tego na Quarkusa?”

    To pytanie padło na spotkaniu technologicznym, które miało być spokojnym podsumowaniem roadmapy. Zamiast tego rozpoczęło jedną z bardziej emocjonalnych dyskusji od czasu, gdy ktoś zaproponował wprowadzenie GraphQL.

    Zacznijmy od początku.

    Spring Boot – działa od 2014

    Jeśli napisałeś w Javie więcej niż Hello World i nie używałeś nigdy Springa, to nie wiem, gdzie się uchowałeś. Ale nawet wtedy debugowałeś pewnie chociaż stack trace wygenerowany przez Spring Boota.

    Ten framework nie próbuje być sexy. Nie próbuje być lean. Chcesz jedną małą rzecz, on wciąga do Twojej aplikacji setki kilobajtów kodu. Ale działa. I to w 99% przypadków wystarczy. Miliony blogpostów, tysiące przykładów na StackOverflow, setki rozszerzeń – to wszystko sprawia, że Spring jest jak stary wygodny fotel: trochę wytarty, trochę trzeszczy, ale wiesz, gdzie co jest.

    Quarkus – działa w 0.04 sekundy

    A potem pojawia się on – Quarkus. Młody, szybki, zgrabny. Wchodzi, robi swoje, znika. Czas startu w trybie natywnym? 0.04s. Spring może o tym tylko pomarzyć. RAM? Żre mniej niż przeglądarka z otwartym StackOverflow. Integracja z GraalVM? Wbudowana.

    Brzmi jak marzenie. Prawda?

    Ok, ale jak to wygląda w kodzie?

    Weźmy przykład z życia wzięty: REST-owy endpoint zwracający zamówienia z bazy. Brzmi jak CRUD? Bo to CRUD.

    Spring Boot

    @RestController
    @RequestMapping("/orders")
    public class OrderController {
      private final OrderRepository repository;
    
      public OrderController(OrderRepository repository) {
        this.repository = repository;
      }
    
      @GetMapping
      public List<Order> getAll() {
        return repository.findAll();
      }
    }
    public interface OrderRepository extends JpaRepository {}

    Odpalasz, działa. H2 w pamięci, logi śmigają, jak coś nie działa, to marudzi już przy starcie, więc nie przeoczysz tego.

    Czas uruchomienia? ~1.5s.

    Quarkus

    @Path("/orders")
    public class OrderResource {
    
        @Inject
        EntityManager em;
    
        @GET
        @Produces(MediaType.APPLICATION_JSON)
        public List<Order> getAll() {
            return em.createQuery("from Order", Order.class).getResultList();
        }
    }
    

    Zero Spring Data. Zero magii. Wszystko jawnie. ORM działa, ale jesteś bliżej metalu. Trochę czujesz, że trochę wróciłeś do czasów Hibernate 3 i pisania SQL-a na kartce.

    Czas uruchomienia? ~0.9s w JVM, ~0.04s w natywie. No robi wrażenie.

    A co z testami?

    W Springu wrzucasz @SpringBootTest i modlisz się, żeby test wstał przed kawą. Ale masz wszystko: kontekst, wstrzykiwanie, nawet MockMvc.

    W Quarkusie masz @QuarkusTest. W teorii działa jak SpringBootTest. W praktyce… działa, ale czasem trzeba przeklikać dokumentację. I nie wszystko działa tak samo.

    Środowiska enterprise

    Jeśli pracujesz w dużej organizacji, gdzie proces release’u ma więcej kroków niż Twój test end-to-end, a każda nowa technologia wymaga pięciu podpisów, to Spring Boot jest bezpiecznym wyborem. Większość aplikacji w korporacjach i tak już działa na Springu. Migracja do Quarkusa w takim miejscu najczęściej kończy się na Proof of Concept – jeśli w ogóle ktoś pozwoli go napisać.

    Spring Boot daje coś, co w enterprise ma ogromną wartość: przewidywalność.
    Każdy nowy developer, konsultant, audytor czy architekt wie, czego się spodziewać. Spring Data, konfiguracja YAML, klasyczny podział warstw – standard, który skraca onboarding i minimalizuje ryzyko.

    Framework dobrze integruje się z narzędziami klasy enterprise: monitoringiem (Actuator + Prometheus/Grafana), systemami logowania, brokerami wiadomości (Kafka, RabbitMQ), systemami autoryzacji (OAuth2, Keycloak), a także korporacyjnymi politykami bezpieczeństwa (czyt. braku internetu na build machine).

    Czas startu aplikacji? W tym środowisku to nie problem – ważniejsze, że działa w sposób powtarzalny. Restart mikroserwisu trwa 2 sekundy dłużej? Nikt tego nawet nie zauważy, bo i tak jest za reverse proxy i load balancerem.

    Startupy

    W startupie liczy się czas. Czas wejścia na rynek, czas testowania hipotezy, czas dostarczenia działającego MVP. Framework ma pomóc, a nie przeszkadzać. I tu sprawa robi się ciekawsza.

    Jeśli zespół zna Springa – to naturalny wybór. Dużo robi się samo: migracje schematów, walidacje, serializacja, dokumentacja (Springdoc/OpenAPI). Mało konfiguracji, dużo gotowych rozwiązań. Dobrze nadaje się do szybkiego prototypowania, o ile ktoś nie zacznie komplikować na siłę (patrz: custom autokonfiguracja w trzecim tygodniu sprintu).

    Ale jeśli zespół ma doświadczenie z cloud-native, zna Dockera, Kubernetes, GraalVM – Quarkus staje się realną alternatywą. Mniejsze zużycie pamięci, krótszy czas uruchamiania, natywne buildy do deployowania na FaaS (np. AWS Lambda) – wszystko to może dać oszczędności w środowisku, gdzie skaluje się przez mnożenie kontenerów.

    Oczywiście: brakuje „magii Springa”, więc część rzeczy trzeba napisać samemu. Ale startupy często i tak piszą własne rozwiązania – tylko szybciej. Tu Quarkus dobrze się wpasowuje: mniej domyślności, więcej jawności. Nie zawsze lepiej, ale bardziej świadomie.

    Mikroserwisy

    Jeśli myślisz o mikroserwisach jako o 30 kopiach tej samej aplikacji z innym endpointem – to Spring Boot wystarczy. Ale jeśli naprawdę projektujesz system rozproszony, z osobnymi cyklami życia serwisów, wysoką skalowalnością i optymalizacją zasobów – tu Quarkus zaczyna być realną opcją.

    Czas startu i zużycie pamięci w Quarkusie to nie tylko ciekawostka z benchmarku – to konkretna oszczędność przy dużej liczbie instancji. W środowisku serverless (FaaS, autoscaling, KNative) różnice robią się zauważalne. Aplikacja, która startuje w 40 ms zamiast 1.5 s i zużywa 70 MB zamiast 200 MB? Na poziomie kilkuset kontenerów dziennie to są wymierne liczby.

    Z drugiej strony: mikroserwisy to nie tylko runtime. To też: testy kontraktowe, logowanie skorelowane po traceId, health checki, obsługa błędów, retry, circuit breakers. Spring Boot ma to wszystko gotowe w Spring Cloud. W Quarkusie musisz to poskładać ręcznie – można, ale trzeba wiedzieć jak.

    W skrócie:

    • Spring Boot to dobry wybór do mikroserwisów klasy „REST + baza + monitoring”, szybko wdrażanych, łatwo rozwijanych.
    • Quarkus warto rozważyć przy większej skali, automatycznym skalowaniu, FaaS lub bardzo wysokich wymaganiach dotyczących czasu reakcji i zużycia zasobów.

    Co warto wiedzieć porównując oba rozwiązania?

    KryteriumSpring BootQuarkus
    Czas uruchamianiaDość szybkiBłyskawiczny (natywny tryb robi robotę)
    Dojrzałość ekosystemuAbsolutna dominacjaJeszcze rośnie, ale ambitnie
    Wsparcie w communityOd „jak to zrobić” po „czemu nie działa”Trochę dziki zachód
    Wersja Hello WorldJedna adnotacja i leciTrochę więcej dłubania
    Sytuacje awaryjneStackOverflow cię nie zawiedzieCzasem trzeba pogrzebać głębiej
    Próg wejściaNiski (albo przynajmniej znajomy)Trochę wyższy
    Praca w zespoleKażdy to znaKtoś musi „nauczyć resztę”

    A co z realnymi projektami?

    Nie widziałem jeszcze dużego polskiego software house’u, który produkcyjnie przeszedł w 100% na Quarkusa. Startupy? Tak. MVP, mikroserwisy, funkcje w chmurze – idealne środowisko. Ale w bankowości, telco czy e-comie? Spring króluje. Bo „działa” jest nadal ważniejsze niż „działa szybko”.

    Co więc wybrać?

    • Jeśli masz do utrzymania system z legacy i 10 letnią historią – Spring Boot.
    • Jeśli tworzysz mikroserwis na serverless z AWS Lambda – Quarkus.
    • Jeśli chcesz szybko dowieźć coś w zespole, który zna Springa – nie kombinuj.
    • Jeśli lubisz eksperymentować i nie boisz się, że trzeba będzie coś napisać samemu – spróbuj Quarkusa.

    Czy warto przechodzić z Spring Boot na Quarkusa?

    W większości projektów – nie. Quarkus nie jest „zamiennikiem” Springa. To framework dla innych przypadków użycia. Tam, gdzie dominują serwisy krótkotrwałe, działające w chmurze i uruchamiane setki razy dziennie – różnica w czasie startu i zużyciu pamięci może mieć znaczenie biznesowe. W projektach korporacyjnych, z rozbudowaną infrastrukturą CI/CD, monitoringiem i długim czasem życia procesów – przewaga Springa nadal jest wyraźna.

  • Piramida testów – czy to jeszcze ma sens?

    W poprzednim artykule opowiedziałem o piramidzie testów. Model ten jest użytecznym uproszczeniem i powinieneś się z nim zapoznać, jeśli jeszcze nie miałeś okazji. 

    Jednak wraz z doświadczeniem, zauważamy coraz więcej niuansów i odstępstw od wyuczonych reguł. Tak samo jest z testowaniem. Piramida testów jest użyteczna, jednak niekiedy istnieje konieczność zmodyfikowania tego podejścia.

    Pojęcie piramidy testów powstało już kilkanaście lat temu, gdy popularna stawała się metodologia TDD. To, że jeszcze o niej pamiętamy, świadczy o tym, że była ona użytecznym drogowskazem dla całego pokolenia programistów. Świat programowania ewoluuje jednak w wariackim tempie. Warto się zastanowić, co się zmieniło przez ten czas w sztuce programowania i czy to nie wymaga od nas zmiany spojrzenia na ten temat.

    Testy są tańsze

    Jak mogłeś przeczytać w poprzednim artykule, jednym z głównych problemów z testami wyższego poziomu (integracyjnymi i E2E) jest czas wykonywania. Wymagają one często uruchomienia całej aplikacji oraz otaczającej jej infrastruktury – bazy danych, kolejek, etc. Niekiedy nie da się (lub nie opłaca) tego zrobić automatycznie i trzeba przygotować takie środowisko testowe ręcznie.

    Choć nadal jest to problem, to już nie tak wielki, jak kilkanaście lat temu. Prawo Moore’a mówi, że moc komputerów podwaja się co 24 miesiące. Nawet jeśli ten rozwój nie jest już tak szybki jak kiedyś, kilkanaście lat to okres, w którym komputery stały się dużo potężniejsze. Do tego takie technologie jak Docker i Test Containers ułatwiły przygotowywanie infrastruktury pod wykonanie testów. Także same frameworki testowe rozwinęły się i testy integracyjne stały się łatwiejsze w pisaniu i wykonaniu, szybsze i bardziej niezawodne. 

    Frameworki rządzą

    Dziś mało kto wyobraża sobie tworzenie kontrolerów HTTP lub dostępu do bazy danych tak, jak robiło się to kilkanaście lat temu. Ilość kodu do napisania jest coraz mniejsza. Duża część jest generowana przez framework na bazie adnotacji lub w inny sposób. Przyniosło to ulgę wielu programistom zmęczonym pisaniu niepotrzebnego, nadmiarowego i powtarzalnego kodu. 

    Jednak taki kod okazuje się często niemożliwy do przetestowania jednostkowo. W Spring Data możemy zdefiniować repozytorium przez stworzenie interfejsu. To, jak działa aplikacja, nie bierze się z kodu, ale konwencji, którą stosuje framework. Nie da się tu wymyślić testu czysto jednostkowego, który byłby użyteczny. Jedyne sensowne podejście, to test integracyjny.

    Nie jest to zjawisko nowe, frameworki towarzyszą nam od lat. Jednak ten trend się rozwija i coraz więcej rzeczy robi się za ich pomocą. 

    100% code coverage wyszło z mody

    Był to trend w początkach TDD, jednak z czasem wiele osób zorientowało się, że pełne pokrycie testami często jest sztuką dla sztuki. Osobiście miałem okazję pracować z osobami, którym testowanie jednostkowe „weszło za mocno”. Efekt? Kod zabetonowany testami, które w praktyce nic nie sprawdzały.

    Testy getterów i setterów, byle tylko dobić do wymaganego przez build poziomu? Brzmi jak świetny pomysł! Jedyne co robi klasa to przekazanie wartości z parametru do kolejnego obiektu? To nic, piszemy test. A że przy okazji takie testy nic nam nie mówią o tym, czy kod robi to, do czego jest napisany? To nic, metryki się przecież zgadzają.

    Co więc z tymi unit testami?

    Nie chcę przekonywać do porzucenia idei testów jednostkowych. Ba, prawdopodobnie to one wciąż powinny być podstawą kontroli jakości Twojej aplikacji. Jak więc sprawić, by wciąż pełniły one swoją rolę, a jednocześnie nie były sztuką dla sztuki i nie prowadziły do zabetonowania aplikacji? Jak wybrać, jakiego rodzaju test w danym miejscu napisać?

    Chciałbym przekonać Cię do traktowania piramidy testów w mniej dogmatyczny sposób. To że piszesz nowy kod, nie musi oznaczać, że 80% testów, jakie napiszesz, to będą testy jednostkowe. Skąd więc to wiedzieć?

    Moduł płytki czy głęboki?

    Chciałbym zapoznać Cię z ideą podziału modułów w aplikacji na głębokie i płytkie. Jest to ciekawa koncepcja, która przydaje się przy dokonywaniu decyzji architektonicznych, jak na przykład właśnie wybór strategii testowania.

    Moduły głębokie 

    Moduł głęboki to taki, w którym duża złożoność jest ukryta za prostym interfejsem. Patrząc z zewnątrz, moduł może wydawać się prosty, jednak logika, która kryje się pod spodem, jest rozbudowana. 

    Przykładem takiego modułu mógłby być kod, który pozwala na wyświetlenie, jaką trasą możemy najbardziej optymalnie przejechać z punktu A do punktu B. Efekt końcowy może być prosty, bo może to być jedynie lista miast, przez które musimy przejechać. Jednak pod spodem dzieje się sporo, od pobrania danych z jakiegoś rodzaju bazy danych przez zaawansowane algorytmy wyznaczania ścieżek do zaprezentowania tego w formie przyjaznej do użytkownika. 

    Ponieważ interfejs jest prosty,  a wnętrze skomplikowane, trudno jest zrozumieć, a także opisać działanie tego modułu, patrząc jedynie z zewnątrz. To jest cecha testów E2E-owych, że testują głównie interfejs modułu lub aplikacji. Dlatego tego rodzaju testy nie będą tu użyteczne. Pewnie warto napisać kilka testów integracyjnych, żeby upewnić się, że całość kodu działa tak, jak tego chcieliśmy. Jednak do testowania logiki użyjemy testów jednostkowych. Pozwalają nam one „wkopać” się do wnętrza modułu i testować jego pojedyncze aspekty w izolacji.

    W modułach głębokich prawdopodobnie tradycyjna piramida testów będzie dobrym rozwiązaniem. W bardzo głębokich modułach możliwe nawet, że testów E2E będzie kilka, a jednostkowych setki. Możliwe też na przykład, że testy integracyjne w ogóle nie będą potrzebne, ponieważ moduł będzie zawierał jedynie czystą logikę biznesową, bez żadnych integracji, więc piramida może być nawet bardziej „agresywna”, niż zwykle, z większą przewagą unit testów

    Moduły płytkie

    Z kolei moduł płytki jest odwrotnością tego pierwszego. Pod interfejsem nie kryje się złożona logika, a zwykle patrząc na sam interfejs można dość łatwo wyobrazić sobie, jaka jest jego implementacja. 

    Przykładem może być moduł, który w odpowiedzi na zapytanie HTTP odczytuje dane z bazy danych i pakuje je w odpowiedź bez nawet szczególnej konwersji na jakąś inną strukturę. W bazie danych jest tabelka z dziesięcioma kolumnami, w odpowiedzi HTTP jest JSON z obiektem z dziesięcioma polami. Typowy CRUD (Create-Read-Update-Delete). Wiadomo, co się dzieje pod spodem i nie są to szczególnie złożone algorytmy, sam kod też jest dość przewidywalny. W tym przypadku raczej będziemy mieli kontroler i repozytorium. Być może znajdzie się jakiś serwis, ale nawet jeśli, to nie robi on wiele więcej, niż wywołanie repozytorium i zwrócenie danych.

    W takiej sytuacji pisanie testów jednostkowych może być wspominaną wcześniej sztuką dla sztuki. Kontroler i repozytorium przetestujemy raczej integracyjnie, zaś test jednostkowy serwisy (o ile w ogóle taki będzie tu istniał) nie powie nam więcej o działaniu modułu, niż dobry test E2E, który sprawdzi, czy całość działa poprawnie i otrzymujemy prawidłową odpowiedź HTTP.

    W takiej sytuacji nasza strategia testowania może być bardzo inna, niż klasyczna piramida testów. W skrajnych przypadkach możliwe, że nie napiszemy ani jednego testu jednostkowego, a pozostaniemy jedynie przy integracyjnych i E2E-owych. Prawdę mówiąc, miałem okazję pracować przy projektach, gdzie nie było żadnych unit testów i nie odczuwałem ich braku (choć w tym podejściu też można przesadzić). Możemy więc skończyć z czymś, co będzie raczej odwróconą piramidą testowania, gdzie im wyższy poziom testów, tym jest ich więcej.

    Jeśli moduł jest płytki, to struktura testów może przybrać nawet kształt odwróconej piramidy

    Prawdziwy świat

    Pewnie moduły, które będziemy pisać, nie będą ani tak głębokie, jak podany przykład aplikacji do wyznaczania trasy, ani tak proste jak wspomniany prosty CRUD. Raczej coś pomiędzy. Dlatego też nasza strategia testowania nie będzie idealną tradycyjną piramidą, ani też odwróconą piramidą. 

    Można sobie na przykład wyobrazić sytuację, gdzie mamy dość złożoną aplikację, ale jej złożoność polega na integracji z wieloma zewnętrznymi systemami. Łączymy się z jakąś bazą danych, potem z kolejką, wysyłamy zapytanie do zewnętrznego API REST-owego i dopiero otrzymujemy wynik. W takiej sytuacji możemy otrzymać system, gdzie najwięcej będzie testów integracyjnych, a tych jednostkowych i E2E-owych będzie stosunkowo niewiele.

    Tego typu struktura testów może mieć sens, jeśli Twój moduł integruje się bardzo mocno z zewnętrznymi systemami

    Co z tym wszystkim zrobić?

    To, co chciałbym, żebyś wyciągnął z tego artykułu, to idea, że niekoniecznie należy się trzymać kurczowo jednej strategii testowania. Raczej przyglądaj się modułom, które piszesz i decyduj, jakie testy najlepiej zapewnią Ci przekonanie, że Twój projekt jest dobrze przetestowany. Do tego przyda Ci się wspomniany wcześniej koncept modułów głębokich i płytkich.

    Z tego wszystkiego można też wyciągnąć wniosek, z którym się zresztą spotykałem, że jakiekolwiek strategie testowania nie mają za bardzo sensu. I tak decyzje o użycie konkretnego typu testu podejmujemy w zależności od tego, jaki typ kodu właśnie piszemy. Czemu by po prostu nie oceniać tego na bieżąco i nie pisać testów, jakie w danej sytuacji uznamy za stosowne? Po co myśleć o tym, jak procentowo rozkłada się udział różnych testów w naszej aplikacji i w jaki kształt to się układa?

    Nie wydaje mi się jednak, żeby całkowite porzucenie tej idei było najlepszym pomysłem. Spoglądanie na strategie testowania pozwala nam nie stracić z oczu dużego obrazka. Wciąż (choćby przybliżone) porównanie, ile mamy testów na którym poziomie, jest przydatną metryką, która pozwala zauważyć, czy idziemy w dobrą stronę. Może zwrócić uwagę na przykład, że trochę za bardzo postawiliśmy na testy E2E i warto kilka z nich przepisać na unit testy. Taka sytuacja zdarzyła mi się zresztą całkiem niedawno.

    Więc nie porzucajmy całkiem piramidy testowania. Jednak zdawajmy sobie sprawę, że tak jak w każdej dziedzinie, dawne wzorce zmieniają się. Pracując w takiej branży jak IT, ignorowanie tego nie jest dobrym pomysłem.