Contactez-nous

Tests des clients REST avec `RestTemplate` ou `WebClient` (`@RestClientTest`)

Maîtrisez le test de vos interactions client REST dans Spring Boot en isolant vos appels externes grâce à @RestClientTest et MockRestServiceServer.

Le défi des tests pour les clients REST

Les applications modernes interagissent fréquemment avec d'autres services via des API REST. Tester les composants qui effectuent ces appels externes (les "clients REST") présente des défis spécifiques. Dépendre de services externes réels pendant les tests peut entraîner une instabilité (le service distant peut être indisponible), des ralentissements (dus à la latence réseau) et des effets de bord indésirables (modification de données sur le service distant).

Pour garantir des tests rapides, fiables et isolés, il est essentiel de pouvoir simuler les réponses des services externes. Nous devons nous assurer que notre client REST construit correctement les requêtes (URL, méthode, en-têtes, corps) et traite correctement les réponses (désérialisation, gestion des erreurs), sans pour autant effectuer de véritables appels réseau.

Spring Boot fournit une annotation de test spécifique, `@RestClientTest`, conçue précisément pour ce besoin, en particulier lors de l'utilisation de `RestTemplate`. Elle permet de tester la logique du client REST de manière ciblée et efficace.

Focalisation sur le client avec `@RestClientTest`

L'annotation `@RestClientTest` est une annotation de "test slice" qui se concentre sur le test des beans clients REST. Contrairement à `@SpringBootTest` qui charge le contexte complet, `@RestClientTest` ne charge qu'un sous-ensemble pertinent de la configuration Spring :

  • Les beans clients REST spécifiés (ou auto-détectés).
  • La configuration nécessaire pour Jackson ou GSON (`@JsonComponent`, etc.) pour la sérialisation/désérialisation.
  • Un `RestTemplateBuilder` configuré.
  • Surtout, elle auto-configure un `MockRestServiceServer` pour intercepter les appels faits via le `RestTemplateBuilder`.

En utilisant `@RestClientTest`, vous pouvez instancier uniquement le client REST que vous souhaitez tester et ses dépendances immédiates liées à la sérialisation, tout en bénéficiant d'un serveur mock prêt à l'emploi pour simuler les réponses HTTP. Cela rend les tests très légers et rapides.

Il est important de noter que `@RestClientTest` est principalement optimisé pour les tests impliquant `RestTemplate` (construit via `RestTemplateBuilder`). Pour `WebClient`, bien que `@RestClientTest` puisse être utilisé pour découper le contexte, la simulation des appels nécessite souvent des approches complémentaires comme l'utilisation de `@MockBean` ou de bibliothèques externes telles que `MockWebServer`.

Tester `RestTemplate` avec `MockRestServiceServer`

Lorsque vous utilisez `@RestClientTest` avec un composant basé sur `RestTemplate`, Spring Boot injecte automatiquement une instance de `MockRestServiceServer`. Ce serveur mock permet de définir des attentes précises sur les requêtes sortantes et de fournir des réponses simulées.

Considérons un service `ExternalServiceClient` qui utilise `RestTemplate` :

@Service
public class ExternalServiceClient {

    private final RestTemplate restTemplate;

    public ExternalServiceClient(RestTemplateBuilder builder) {
        // Il est recommandé d'injecter RestTemplateBuilder et de construire RestTemplate
        this.restTemplate = builder.rootUri("http://example.com/api").build();
    }

    public UserData fetchUserData(String userId) {
        String url = "/users/{id}";
        try {
            ResponseEntity response = restTemplate.getForEntity(url, UserData.class, userId);
            return response.getBody();
        } catch (RestClientException e) {
            // Gérer les erreurs...
            return null;
        }
    }
    // Classe interne ou externe pour les données utilisateur
    public static class UserData { 
        public String id;
        public String name;
    }
}

Voici comment tester ce client avec `@RestClientTest` et `MockRestServiceServer` :

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;

import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
import static org.assertj.core.api.Assertions.assertThat;

@RestClientTest(ExternalServiceClient.class) // Cible le test sur ExternalServiceClient
class ExternalServiceClientTest {

    @Autowired
    private ExternalServiceClient client; // Le bean sous test est injecté

    @Autowired
    private MockRestServiceServer server; // Le serveur mock est injecté

    @Autowired
    private RestTemplateBuilder builder; // Le builder est disponible si besoin

    @Test
    void fetchUserData_shouldReturnUserData_whenApiCallIsSuccessful() {
        // Arrange: Définir l'attente sur le serveur mock
        String expectedUserId = "123";
        String responseBody = "{\"id\":\"123\", \"name\":\"Test User\"}";

        this.server.expect(requestTo("http://example.com/api/users/" + expectedUserId)) // Attend une requête vers cette URL
                   .andExpect(method(HttpMethod.GET)) // Attend une méthode GET
                   .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); // Répond avec succès et le corps JSON

        // Act: Appeler la méthode du client
        ExternalServiceClient.UserData userData = client.fetchUserData(expectedUserId);

        // Assert: Vérifier le résultat
        assertThat(userData).isNotNull();
        assertThat(userData.id).isEqualTo(expectedUserId);
        assertThat(userData.name).isEqualTo("Test User");

        // Vérifier que toutes les attentes ont été satisfaites
        this.server.verify();
    }

    @Test
    void fetchUserData_shouldReturnNull_whenApiReturnsError() {
        // Arrange
        String expectedUserId = "999";

        this.server.expect(requestTo("http://example.com/api/users/" + expectedUserId))
                   .andExpect(method(HttpMethod.GET))
                   .andRespond(withServerError()); // Simule une erreur 500

        // Act
        ExternalServiceClient.UserData userData = client.fetchUserData(expectedUserId);

        // Assert
        assertThat(userData).isNull(); // Ou vérifier la gestion d'erreur spécifique
        this.server.verify();
    }
}

Dans cet exemple, `server.expect()` définit quelle requête est attendue (URL, méthode). `andRespond()` définit la réponse simulée (succès, erreur, corps, en-têtes). `server.verify()` à la fin assure que l'appel attendu a bien été effectué par le client.

Approches pour tester `WebClient`

Comme mentionné, `@RestClientTest` n'auto-configure pas de `MockRestServiceServer` pour `WebClient`. Cependant, on peut toujours l'utiliser pour bénéficier du test slice et de la configuration de Jackson/GSON.

Une approche courante consiste à utiliser `@MockBean` pour mocker directement le `WebClient` ou son `Builder`. Cela permet de contrôler les appels de manière programmatique avec Mockito, mais ne teste pas la construction réelle des requêtes HTTP ni la désérialisation effectuée par `WebClient` lui-même.

// Exemple simplifié avec @MockBean (ne teste pas l'interaction HTTP réelle)
// Nécessite une configuration plus complexe pour mocker les appels fluides de WebClient

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.assertThat;

@RestClientTest(MyWebClientService.class) // Toujours utile pour le slice
class MyWebClientServiceMockBeanTest {

    @Autowired
    private MyWebClientService service;

    @MockBean // Mocker le builder
    private WebClient.Builder webClientBuilderMock;

    // Mocker toute la chaîne d'appel de WebClient est complexe...
    // @MockBean sur WebClient directement est parfois plus simple si possible.

    @Test
    void testServiceLogicWithMockedWebClient() {
        // ... Configuration complexe des mocks pour WebClient ...
        // WebClient webClientMock = WebClient.builder().build(); // Simulé
        // when(webClientBuilderMock.build()).thenReturn(webClientMock);
        // Configurer les mocks pour .get(), .uri(), .retrieve(), .bodyToMono(), etc.
        
        // Act & Assert
        // ... 
    }
}

Une alternative plus robuste, si l'on veut tester l'interaction HTTP simulée, est d'utiliser une bibliothèque comme OkHttp `MockWebServer`. On peut l'intégrer manuellement dans le test (même en utilisant `@RestClientTest` pour le slicing). On configure alors le `WebClient.Builder` pour pointer vers l'URL du `MockWebServer` démarré localement. Cela permet de définir des réponses attendues sur le `MockWebServer` et de vérifier les requêtes reçues, offrant un niveau de test similaire à `MockRestServiceServer` pour `RestTemplate`.

Le choix entre `@MockBean` et `MockWebServer` dépend du niveau de test souhaité : `@MockBean` est plus simple pour isoler rapidement la logique du service, tandis que `MockWebServer` offre un test plus fidèle de l'interaction HTTP du `WebClient`.

Bénéfices et meilleures pratiques

L'utilisation de `@RestClientTest` (principalement avec `RestTemplate`) et des techniques de simulation associées offre plusieurs avantages : Isolation (les tests ne dépendent pas des services externes), Rapidité (pas d'appels réseau), Fiabilité (les tests ne cassent pas si le service externe change ou est indisponible) et la possibilité de tester finement la logique de construction des requêtes et de traitement des réponses (y compris la sérialisation/désérialisation).

Il est crucial de se rappeler que ces tests ne valident pas le comportement réel du service externe ni les problèmes potentiels liés au réseau (timeouts, etc.). Ils valident uniquement que votre client interagit comme prévu avec ce qu'il *croit* être le service externe.

Utilisez `@RestClientTest` lorsque vous voulez spécifiquement tester votre logique client REST (`RestTemplate`). Complétez ces tests par quelques tests d'intégration de plus haut niveau (parfois appelés tests de contrat ou tests end-to-end légers) qui effectuent de réels appels sur des environnements de test dédiés pour valider l'intégration réelle avec les services externes.