Dev-Mind

Tester Spring WebFlux WebClient en Junit 4 ou 5 avec MockWebServer

15/01/2018
Java  Junit  Spring Boot  WebClient 

En jouant avec le dernier framework WebFlux de Spring et notamment Web Client, j’ai découvert la librairie okhttp écrite par la société Square. Square est spécialisé dans le paiement électronique et ils mettent à disposition des commerçants des mini lecteur de carte de crédits. Ils développement beaucoup pour toutes les plateformes mobiles et notamment pour Android et donc indirectement Java.

MockServer et WebClient

Okhttp est un mini client HTTP et vous trouvez aussi dans ce projet un mini serveur "mockable" que vous pouvez utiliser dans vos tests.

Tester Web Client en Junit4

Prenons un exemple Spring WebFlux utilisant Web Client

@Component
public class ElasticMailSender implements EmailSender {

    private WebClient webClient;

    public ElasticMailSender() {
        webClient = WebClient.create("https://api.elasticemail.com");
    }

    public ElasticMailSender(WebClient webClient) {
        this.webClient = webClient;
    }

    @Override
    public void send(EmailMessage email) {
        ElasticEmailResponseDTO response = webClient.post()
            .uri(String.format("/%s/email/send", "v2"))
            .body(BodyInserters
                 .fromFormData("apikey", "MYAPISECRET")
                 .with("from", "guillaume@dev-mind.fr")
                 .with("fromName", "Dev-Mind")
                 .with("to", email.getTo())
                 .with("subject", email.getSubject())
                 .with("isTransactional", "true")
                 .with("body", email.getContent())
            )
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(ElasticEmailResponseDTO.class)
            .block();

        if (response.getSuccess() == false) {
            throw new RuntimeException(response.getError());
        }
    }
}

Si nous voulons tester cette classe nous devons simuler le comportement de WebClient. Nous pouvons utiliser un framework de mock mais dans ce cas là nous ne testons pas le flux HTTP. Utilisons donc un simulacre de serveur web…​ C’est là que rentre en compte MockWebServer

Pour l’utiliser rien de plus simple. Commencez par insérer cette dépendance dans votre build Gradle

testCompile("com.squareup.okhttp3:mockwebserver:3.9.1")

MockWebserver est en fait une Rule Junit 4 et étend la classe ExternalResource. Votre test peut s’écrire de cette manière

public class ElasticMailSenderTest {
    @Rule
    public MockWebServer server = new MockWebServer();
    private WebClient webClient;
    private ElasticMailSender elasticMailSender;

    @Before
    public void setUp(){
        this.webClient = Mockito.spy(WebClient.create(this.server.url("/").toString()));
        elasticMailSender = new ElasticMailSender(webClient);
    }

    @Test
    public void send() {
        prepareResponse(response -> response
                .setHeader("Content-Type", "application/json")
                .setBody("{ \"success\" : true }"));

        elasticMailSender.send(new EmailMessage(
                "guillaume@test.fr",
                "Email test",
                "<h1>Hi Guillaume</h1><p>Waow... you are able to send an email</p>")
        );

        verify(webClient, atLeastOnce()).post();
    }

    @Test
    public void sendWithError() {
        prepareResponse(response -> response
                .setHeader("Content-Type", "application/json")
                .setBody("{ \"success\" : false, \"error\" : \"error expected\" }"));

        assertThatThrownBy(() -> elasticMailSender.send(new EmailMessage(
                "guillaume@test.fr",
                "Email test",
                "<h1>Hi Guillaume</h1><p>Waow... you are able to send an email</p>")))
                .isExactlyInstanceOf(RuntimeException.class)
                .hasMessage("error expected");
    }

    private void prepareResponse(Consumer<MockResponse> consumer) {
        MockResponse response = new MockResponse();
        consumer.accept(response);
        this.server.enqueue(response);
    }
}

Une fois que la Rule est créée on initialise un WebClient avec une URL qui sera servie par MockWebServer

WebClient.create(this.server.url("/").toString())

Ensuite la méthode prepareResponse() permet de constuire une réponse qui sera renvoyée quand WebClient appelera cette URL.

Jusque là tout va bien mais que ce passe t’il si nous voulons passer à Junit 5 ?

Tester Web Client en Junit5

Si vous souhaitez utiliser Junit 5 dans votre application vous pouvez commencer par lire mon article sur le sujet :-). Pour ne plus avoir de dépendance vers des anciennes versions de Junit, vous pouvez ajouter à votre projet Gradle cette configuration

configurations {
	all {
		exclude module: "junit"
	}
}
testCompile("org.junit.jupiter:junit-jupiter-api")
testRuntime("org.junit.jupiter:junit-jupiter-engine")

Mais dans ce cas là vous ne pourrez plus utiliser la librairie précédente car elle a besoin de Junit 4 pour compiler. Il faut savoir que les versions 5 et 4 ne sont pas rétrocompatibles et les Rule Junit4 ont été remplacées par des extensions dans Junit 5.

Junit 5 a été réécrit pour profiter pleinement de Java 8. Le support Java 8 est encore à ses débuts dans Android, et Square ne va pas faire évoluer tout de suite sa librairie pour être compatible Junit 5. Pour palier à ce problème vous pouvez utiliser le fork mis en place par Dev-Mind. Ce projet utilise le projet okhttp mais ne dépend pas de Junit 4, et propose deux extensions pour vos tests.

Vous pouvez charger cette librairie sur Maven Central. Pour l’utiliser dans un projet Gradle vous pouvez déclarer cette dépendance

testCompile("com.devmind:mockwebserver:0.1.0")

La première extension MockWebServerExtension se charge d’instancier un serveur web, de le démarrer et de l’arrêter avant et après chaque test.

@ExtendWith(MockWebServerExtension.class)
class MySpringWebfluxServiceTest {

    private MockWebServer server;
    private WebClient webClient;
    private MySpringWebfluxService service;

    @BeforeEach
    public void setup(MockWebServer server) {
        this.webClient = WebClient.create(server.url("/").toString());
        this.service = new MySpringWebfluxService(webClient);
        this.server = server;
    }

    @Test
    public void mytest() throws Exception {
        prepareResponse(response -> response
                .setHeader("Content-Type", "application/json")
                .setBody( "{\n" +
                          "  \"error_message\" : \"The provided API key is invalid.\",\n" +
                          "  \"predictions\" : [],\n" +
                          "  \"status\" : \"REQUEST_DENIED\"\n" +
                          "}"));

        StepVerifier.create(service.myMethod())
                .expectComplete()
                .verify(Duration.ofSeconds(3));
    }

    private void prepareResponse(Consumer consumer) {
        MockResponse response = new MockResponse();
        consumer.accept(response);
        this.server.enqueue(response);
    }

}

Avec la deuxième extension MockSimpleWebServerExtension plus basique, vous gérez vous même l’arrêt relance du serveur. Ceci permet par exemple de lancer le serveur avant le lancement de tous les tests et de l’arrêter à la fin de l’exécution

@ExtendWith(MockSimpleWebServerExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MySpringWebfluxServiceTest {

    private MockWebServer server;
    private WebClient webClient;
    private MySpringWebfluxService service;

    @BeforeAll
    public void init(MockWebServer server) throws IOException {
        server.start();
        this.server = server;
    }

    @AfterAll
    public void tearDown() throws IOException {
        server.shutdown();
    }

    @BeforeEach
    public void setup(MockWebServer server) {
        this.webClient = WebClient.create(server.url("/").toString());
        this.service = new MySpringWebfluxService(webClient);
    }

    @Test
    public void mytest() throws Exception {
        prepareResponse(response -> response
                .setHeader("Content-Type", "application/json")
                .setBody( "{\n" +
                          "  \"error_message\" : \"The provided API key is invalid.\",\n" +
                          "  \"predictions\" : [],\n" +
                          "  \"status\" : \"REQUEST_DENIED\"\n" +
                          "}"));

        StepVerifier.create(service.myMethod())
                .expectComplete()
                .verify(Duration.ofSeconds(3));
    }

    private void prepareResponse(Consumer<MockResponse> consumer) {
        MockResponse response = new MockResponse();
        consumer.accept(response);
        this.server.enqueue(response);
    }
}

Voila vous n’avez plus d’excuse pour ne pas tester vos services Spring utilisant WebClient en Junit 5. Le fork proposé par Dev-Mind peut être utilisé en attendant que Square mette à jour sa librairie.


Article précédent : Utiliser Junit 5 dans une application Spring Boot
Article suivant : Publier une librairie open source sur Maven Central Vous pouvez laisser un commentaire ou poser une question sur cet article par mail ou sur twitter.