Test poprawności access tokena z API przy użyciu WireMock

1

Cześć,
mam funkcjonalność, w której aplikacja komunikuje się z API Spotify w celu uzyskania access tokena dla użytkownika.

Chciałbym napisać test do tej funkcjonalności, korzystając z WireMocka. Mockuję odpowiedź API, zwracając JSON zapisany w pliku.

W teście chciałbym sprawdzić, czy metoda faktycznie zwraca poprawny access token.

Pomyślałem, że pobiorę access token z odpowiedzi zwróconej przez metodę i porównam z access tokenem ręcznie pobranym z pliku JSON. Czy takie podejście jest dobre?

@Test
void whenRetrieveToken_ReturnExpectedToken() throws IOException {
    mockApiResponse(200, response);

    Result <String> result = accessTokenHandler.retrieveSpotifyAccessToken("auth_code", generateJwtTokenForUser());

    assertThat(result.data()).isEqualTo(expectedToken());
}

private void mockApiResponse(int statusCode, String response) {
    wireMockServer.stubFor(post("/v1/spotify/auth/api/token")
            .willReturn(aResponse()
                    .withStatus(statusCode)
                    .withHeader("Content-Type", "application/json")
                    .withBody(response)));
}

private String expectedToken() throws IOException {
    String expectedToken;
    try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("spotify_access_token_response.json")) {
        expectedToken = jsonFieldExtractor.getFieldValueFromJson(inputStream, "access_token", String.class);
    }
    return expectedToken;
}
public class JsonFieldExtractor {
    private final ObjectMapper objectMapper;
    public JsonFieldExtractor(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    public <T> T getFieldValueFromJson(InputStream inputStream, String fieldPath, Class<T> valueType) throws IOException {
        JsonNode jsonNode = objectMapper.readTree(inputStream);

        JsonNode fieldValue = jsonNode.at("/" + fieldPath.replace(".", "/"));

        if (fieldValue.isMissingNode()) {
            throw new IllegalArgumentException(String.format("Field '%s' not found in JSON input", fieldPath));
        }

        return objectMapper.treeToValue(fieldValue, valueType);
    }
}
3

Według mnie ten test testuje jedynie to, czy Twoja metoda accessTokenHandler.retrieveSpotifyAccessToken woła URL /v1/spotify/auth/api/token.

1

Myślę, że w porządku, też tak robię testy moich funkcjonalności.

1

Moim zdaniem to się średnio opłaca. Za bardzo test przywiązany jest do struktury API spotify, nie jest Ci to na rękę. Wygląda to trochę tak, jakbyś chciał swoimi testami sprawdzić, czy Spotify przyznał Ci dostęp, a tak się nie da.

Do czego chcesz użyć tego access tokenu który wyciągasz?

2

Ten kodzik z pewnością przejdzie typowe korporeview w stylu "hmm, a napisał testy? A, coś jest, jest adnotacja @Test, no to approve, następny.". 😉

Co samo w sobie ma jakąś wartość (zaliczone review), ale w praktyce niewiele daje. Nie dałeś całego kodu, ale jeśli dobrze rozumiem:

  1. instruujesz mocka, żeby pod urlem /x zwrócił dany response y
  2. odpytujesz mocka o url /x
  3. sprawdzasz czy zwróciło się y

Czy to ma sens?

0
Riddle napisał(a):

Moim zdaniem to się średnio opłaca. Za bardzo test przywiązany jest do struktury API spotify, nie jest Ci to na rękę. Wygląda to trochę tak, jakbyś chciał swoimi testami sprawdzić, czy Spotify przyznał Ci dostęp, a tak się nie da.

Do czego chcesz użyć tego access tokenu który wyciągasz?

Potrzebuję tego tokenu do utworzenia playlisty. Token zawiera uprawnienia.

0
Ornstein napisał(a):

Do czego chcesz użyć tego access tokenu który wyciągasz?

Potrzebuję tego tokenu do utworzenia playlisty. Token zawiera uprawnienia.

No widzisz, więc to jest Twoja faktyczna funkcjonalność którą powinieneś testować. "Wyciąganie tokenu" to jest tylko implementacja tego.

@Ornstein W jaki sposób Twoja aplikacja będzie tworzyć tą playlistę? Użytkownik będzie wybierał piosenki, czy jak?

0
Riddle napisał(a):
Ornstein napisał(a):

Do czego chcesz użyć tego access tokenu który wyciągasz?

Potrzebuję tego tokenu do utworzenia playlisty. Token zawiera uprawnienia.

No widzisz, więc to jest Twoja faktyczna funkcjonalność którą powinieneś testować. "Wyciąganie tokenu" to jest tylko implementacja tego.

Ok. Na ten moment podzieliłem to w taki sposób, że osobno obsługuję proces logowania do Spotify. Jeśli zakończy się on sukcesem, zapisuję token w Redis, a jako klucz wykorzystuję ID użytkownika pobrane z JWT. Osobno będzie tworzenie playlisty.

Riddle napisał(a):

@Ornstein W jaki sposób Twoja aplikacja będzie tworzyć tą playlistę? Użytkownik będzie wybierał piosenki, czy jak?

Na podstawie pogody w podanej przez użytkownika lokalizacji.

1
Riddle napisał(a):

@Ornstein W jaki sposób Twoja aplikacja będzie tworzyć tą playlistę? Użytkownik będzie wybierał piosenki, czy jak?

Na podstawie pogody w podanej przez użytkownika lokalizacji.

No okej, to się wydaje główną logiką programu. W tym miejscu powinieneś testować głównie.

Widzę że działasz systemem:

  1. chcesz funkcję
  2. myślisz jak ją zaprogramować
  3. znajdujesz sposób (np. ten access token)
  4. i myślisz jak teraz przetestować ten access token.

To jest słabe podejście. Lepiej byłoby na to popatrzeć tak:

  1. Chcesz funkcję, np. żeby program utworzył playlistę na podstawie pogody.
  2. I pod to piszesz test! Nie pod Twoją implementację którą sobie wymyśliłeś.
  3. Na przykład: given pogoda jest śniegowa, kiedy tworzę playlistę, na playliście jest piosenka "ain't no sunshine when she's gone".
  4. Wszystkie pozostałe rzeczy, takie jak tokeny, jwt, id, wszystko to można zaimplementować polimorficznie później.

Ja bym stworzył taki test:

void playlistContainsSong_basedOnWeather_inUserLocation() {
  userInLocation("Kraków");
  weatherInLocation("Kraków", WeatherEnum.SNOW);
  generatePlaylist();
  assertContains("Aint' no sunshine when she's gone", playlistSongs());
}

Pytanie które ja mam, to czy Twój program ma jakąś logikę, np. decyduje jakimś algorytmem jaka piosenka ma być na podstawie jakiej pogody? Czy może ma być tylko takim "gołym" routerem requestów do spotify'a, czyli w zasadzie napisałbyś sobie klienta Spotify? 😄

Ornstein napisał(a):

Ok. Na ten moment podzieliłem to w taki sposób, że osobno obsługuję proces logowania do Spotify. Jeśli zakończy się on sukcesem, zapisuję token w Redis, a jako klucz wykorzystuję ID użytkownika pobrane z JWT. Osobno będzie tworzenie playlisty.

No spoko, możesz tak zrobić oczywiście, tylko szczerze mówiąc nie za wiele sensu widzę w pisaniu testów pod to, bo i tak nie kontrolujesz tego czy dostaniesz ten access token czy nie. Jak np. dostaniesz bana w spotify, to i tak nie masz żadnej kontroli nad tym, więc co Ci to da że te testy by nie przeszły?

Testy powinno się pisać pod rzeczy na które mamy wpływ, czyli w zasadzie tylko to co jest w ramach naszego projektu. Oczywiście możesz sobie zaprogramować co Twój program ma zrobić, jak spotify Cię odrzuci; ale to program powinien obsłużyć, nie testy.

Myślę że testy które napisałeś, tak jak mówił wcześniej @kelog po prostu testują czy mock zwrócił to co zamockowałeś 😉

0
Riddle napisał(a):

Pytanie które ja mam, to czy Twój program ma jakąś logikę, np. decyduje jakimś algorytmem jaka piosenka ma być na podstawie jakiej pogody? Czy może ma być tylko takim "gołym" routerem requestów do spotify'a, czyli w zasadzie napisałbyś sobie klienta Spotify? 😄

Algorytm jest w trakcie implementacji. Nie będzie tutaj cudów - jeśli pogoda będzie ponura, to muzyka typowo depresyjna. Jeśli będzie burza to jakiś mózgotrzep itp.

1
Ornstein napisał(a):

Algorytm jest w trakcie implementacji. Nie będzie tutaj cudów - jeśli pogoda będzie ponura, to muzyka typowo depresyjna. Jeśli będzie burza to jakiś muzgotrzep itp.

Okey, no widzisz. To ja Ci proponuję takie podejście:

Co do logiki aplikacji - pisz testy pod swój algorytm. Testy powinny wiedzieć o takich rzeczach jak: pogoda, lokacja, piosenka (tytuł), playlista (jako lista piosenek), humor, gatunek muzyki, rodzaj pogody. Testy nie powinny wiedzieć że piosenka jest ze spotify, nie powinny wiedzieć że playlista jest ze spotifya, najlepiej nic o spotify nie powinny wiedzieć. Możesz się posilkować kodem który podrzuciłem wyżej. Implementacja algorytmu nie powinna korzystać ze spotify'a bezpośrednio, ani nawet nie powinna korzystać z klienta http bezpośrednio. nie powinna też wiedzieć nic o tokenach. Schowaj całą integrację ze spotify za abstrakcją, np. za interfejsem:

interface SpotifyIntegration {
  void login();
  boolean tryLoginOrFailure(); // return true on success, false on failure

  List<Integer> listSongIds();
  List<String> listPlaylistNames();
  void addSongToPlaylist(int songId, int playlistId); // int, long, String, typ sobie wybierz
}

Ewentualnie jak chcesz zachować gdzieś swój token, to wystaw go za pomocą interfejsu:

interface SpotifyIntegration {
  String authenticateAndReturnToken();               // login and return access token
  void authenticateUsingExistingToken(String token); // use existing access token

  List<Integer> listSongIds();
  List<String> listPlaylistNames();
  void addSongToPlaylist(int songId, int playlistId); // int, long, String, typ sobie wybierz
}

I nic więcej. Wszelkie id, tokeny, etc. powinny być schowane w klasie implementacującej interfejs.

Co do integracji ze spotify - wiadomo że musisz ją jakoś lokalnie sprawdzić, czy działa, czy się ładuje. Sprawdzaj to lokalnie, ale testów pod to nie ma za bardzo sensu pisać. Na 99% Ci będą tylko przeszkadzać a nie pomagać. Żeby na prawdę one miały sens, takie akceptacyjne testy, to musiałbyś w gruncie rzeczy postawić lokalny sandbox spotify'a żeby móc do niego strzelać. No albo możesz też strzelać do prawdziwego spotify'a, ale to jest proszenie się o kłopoty bo testy będą failować z przyczyn niezależnych od Ciebie i przestaną być pomocne.

FAQ:

  • Ale czy to znaczy że jak będę miał buga w integracji ze spotify, to moje testy tego nie znajdą? No niestety tak 😕 Nie za bardzo jest sposób żeby to przetestować inny niż strzelenie do spotify'a albo do jakiegoś sandboxa spotify. Bez tego jedyne co możesz stworzyć to testy które testują mocki, tylko one są średnio pomocne, bo nie zawiele pomagają, trochę przeszkadzają, i niedostatecznie dobrze specyfikują zachowanie. Tak na prawdę takie testy testują tylko tyle że kod który napisałeś to kod który napisałeś.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.