Kategorie
java

Netflix DGS Testy z Hamcrest

Netflix DGS coraz bardziej mi się podoba, ale do pełni szczęścia chciałbym zobaczyć, jak wyglądają z nim testy. Ale czy i tym razem biblioteka zaskoczy mnie pozytywnie? Hm… no właśnie, chyba będzie lekkie zaskoczenie.

junior-hava-developer-handbook-what-to-know

Netflix DGS i testy Query

W ramach testów chcielibyśmy odpytać endpoint GraphQL o nasze dane. Do tego celu Netflix przygotował klasę DgsQueryExecutor. Jest to jedna z głównych klas biblioteki i służy nam do wywoływania zapytań GraphQL bez konieczności włączania serwera webowego.

W naszym przypadku chcemy przetestować działanie endpointa do pobierania danych o projektach.

Pamiętamy, ze nasza aplikacja działa w kontekście Springa. Dlatego testy będą potrzebowały uruchomionego kontenera. Nie potrzebują jednak uruchamiać wszystkiego (np. przy dużej aplikacji), a tylko fragment, który nas interesuje. Stąd wymienione klasy przy okazji adnotacji @SpringBootTest.

Szkielet naszej klasy z testami wyglądałby następująco:

@SpringBootTest(classes = {DgsAutoConfiguration.class, ProjectFetcher.class})
class ProjectGraphQLTest {
  @Autowired
  DgsQueryExecutor dgsQueryExecutor;

// ...
// ...
// ...
}

Tutaj mała uwaga na marginesie. Warto zastanowić się, czy test powinien wiedzieć, że do jego realizacji potrzebne ProjectFetcher.class. Przecież testujemy działanie endpointa GraphQL, także czy nas interesuje, jaka klasa zapewni jego działanie?

Napiszmy teraz dwa testy. Pierwszy pobiera projekty z ich id, name oraz description. Pamiętamy, z poprzedniego artykułu, że nasze dane testowe w klasie DATA miały te pola wypełnione.

Przy okazji wykorzystamy matchery z biblioteki Hamcrest, która jeszcze do tej pory na blogu się nie pojawiała.

   @Test
  void nonNullProperties() {
    List<Project> projects = dgsQueryExecutor.executeAndExtractJsonPathAsObject(
        "{ projects { id, name, description } }",
        "data.projects",
        new TypeRef<>() {}
    );

    assertThat(projects, hasSize(4));
    assertThat(projects, everyItem(hasProperty("id", notNullValue())));
    assertThat(projects, everyItem(hasProperty("name", notNullValue())));
    assertThat(projects, everyItem(hasProperty("description", notNullValue())));
  }

Drugi test będzie podobny. Z tym, że tym razem pytamy tylko o id projektów. Dlatego oczekujemy, że pozostałe pola nie zostaną wypełnione. Czyli będą puste.

  @Test
  void nullProperties() {
    List<Project> projects = dgsQueryExecutor.executeAndExtractJsonPathAsObject(
        "{ projects { id } }",
        "data.projects",
        new TypeRef<>() {}
    );

    assertThat(projects, hasSize(4));
    assertThat(projects, everyItem(hasProperty("id", notNullValue())));
    assertThat(projects, everyItem(hasProperty("name", nullValue())));
    assertThat(projects, everyItem(hasProperty("description", nullValue())));
  }

Uważaj na API

W testach korzystamy z metody executeAndExtractJsonPathAsObject klasy DgsQueryExecutor. Dlaczego akurat z tej metody?

Przede wszystkim chciałbym w łatwy sposób napisać asercje. A najłatwiej się je pisze, gdy mamy jasno określone typy obiektów.

Jasne, ale dlaczego jednak nie skorzystamy z metody przedstawionej w obecnej dokumentacji na Github Netflix DGS? Czyli executeAndExtractJsonPath.

Teoretycznie poniższy fragment powinien działać, kompilator nie zwróci błędów. Wydaje się, że wszystko jest ok. Ale czy na pewno?

    List<Project> notWorkingTypeConversion = dgsQueryExecutor.executeAndExtractJsonPath(
        "{ projects { id, name, description } }",
        "data.projects"
    );

Tak naprawdę jednak to nie zadziała poprawnie. W runtime zmienna notWorkingTypeConversion będzie typem net.minidev.json.JSONArray. A co za tym idzie asercje zaczną „wariować”, bo obiekt, który do nich wpadnie, nie będzie miał odpowiednich metod i pól. Sygnatura metody (DGS jest napisany w Kotlinie) trochę nas w tym przypadku oszukała:

fun <T> executeAndExtractJsonPath(query: String, jsonPath: String):T

Jeśli poszukamy trochę głębiej w kodzie, to możemy zauważyć, że typ T nigdy nie będzie sprawdzany. I gdzieś w odmętach bibliotecznych po prostu net.minidev.json.JSONArray zostanie zrzutowany na niego. A błędu w runtime nie będzie widać od razu, bo… net.minidev.json.JSONArray rozszerza standardową ArrayList, czyli po prostu jest zwykłą listą z interfejsem List. Stąd ostateczne przypisanie do List<Project> się powiedzie, bo runtime nie sprawdzi tu typu generycznego (nic o nim nie wie).

Jeszcze raz o executeAndExtractJsonPathAsObject

Osoby programujące często z JSON w Javie prawdopodobnie zauważą podobieństwa wywołania tej metody do Jacksona:

    List<Project> projects = dgsQueryExecutor.executeAndExtractJsonPathAsObject(
        "{ projects { id, name, description } }",
        "data.projects",
        new TypeRef<>() {}
    );

I dokładnie stąd się bierze taka forma tej metody. Netflix DGS skorzysta w tym przypadku pod spodem z Jacksona do deserializacji pojedynczego obiektu. A skoro tak będzie, to musimy zadbać o to, żeby Jackson umiał ten obiekt poprawnie zbudować. Stąd kilka adnotacji.

    @JsonCreator
    public Project(
        @JsonProperty("id") String id,
        @JsonProperty("name") String name,
        @JsonProperty("description") String description) {
        this.id = id;
        this.name = name;
        this.description = description;
    }

Czego nie bierz z dokumentacji?

Z dokumentacją jest zawsze tak samo. Zawiera pewne proste elementy i … fragmenty kodu, które nie zawsze są najlepszymi praktykami. One po prostu pokazują, jak działa dana biblioteka, czy framework.

Dla przykładu. Pamiętasz może, jak wyglądały kiedyś fragmenty kodu większości projektów ze Spring Framework? Były tam pakiety dla różnych warstw model, service i controller. Nie ważne, że to nie ma zwykle sensu. Nie ważne, że układ warstw w dużych projektach to olbrzymi bałagan w logice domenowej. Ważne, że setki, jeśli nie tysiące projektów, zostały napisane w taki sposób. Bo było tak w dokumentacji. I schemat został powielony. Nie ważne, że błędny.

Dlatego dokumentację należy przeglądać z pewną dozą sceptycyzmu. Co powinny robić testy GraphQL z Netflix DGS?

Przede wszystkim testować wejście i wyjście z endpointów. A tu dużo nam nie trzeba. W zasadzie cała magia kryje się w DgsQueryExecutor i tyle. I właśnie dokładnie tyle powinniśmy na tę chwilę brać z dokumentacji.

Dlatego, jeśli przeglądasz stronę Testing na github projektu Netflix DGS to zwróć uwagę, że przedstawione tam testy zaczynają wiedzieć za dużo o implementacji.

Przykładem tego jest sekcja Mocking External Service Calls in Tests. Fajnie, że pokazuje możliwość wykorzystania atrap obiektów, ale … czy ten kod w ogóle powinien wiedzieć, jak wykonywane są zapytania do usług w tle? Raczej nie sądzę.

Podsumowanie

Fajnie. Wyszliśmy od testów. A przy okazji zahaczyliśmy o temat wprowadzającego w błąd API. I zadaliśmy kilka pytań o świadomość testów odnośnie wewnętrznej implementacji systemu.

Czy to API DgsQueryExecutor wpłynie na moją ocenę Netflix DGS? Raczej minimalnie. Przecież sama biblioteka robi dobrą robotę. Jak widać ma też trochę pola do poprawy i wygładzenia.

Także testy z Netflix DGS są możliwe. I są stosunkowo proste… jak już wiesz, jak je poprawnie napisać.

A może też miałaś, czy miałeś, taki przypadek z API wprowadzającym w błąd? Być może też zastanawiasz się nad tym, ile powinny wiedzieć testy o wewnętrznej implementacji aplikacji? Podziel się tym w komentarzu.

0 0 vote
Article Rating
Subscribe
Powiadom o
guest
3 komentarzy
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Arek
Arek
13 dni temu

Cześć, dzięki za wpis.
Mam pewną sugestię dotyczącą bloga: szerokość tekstu na moim monitorze 16:10 to na oko około 30%. Bardzo niewygodnie się przez to czyta bo co chwila trzeba scrolować w dół.

Ps. zamierzałem załączyć grafikę, obrazującą jak źle to wygląda ale coś nie działa, nie chce się załadować.

Last edited 13 dni temu by Arek
Arek
Arek
1 dzień temu

Szczerze mówiąc to nie odczułem żadnej poprawy tzn. tekst dalej zajmuje max 30% szerokości strony. Może to jakiś trend/moda nie wiem ale dla mnie niewygodnie się to czyta bo nawet listingi kodu się nie mieszczą w pełni i trzeba je ręcznie przewijać.