This time I will show you how to combine Webflux WebClient along with Cucumber and Junit 5 to consume GitHub API v3 public REST API. First, let’s start creating a new Spring Boot project with Webflux and Lombok as dependencies:
spring init --dependencies=webflux,lombok --build=gradle --language=java spring-boot-web-client
Here is the complete build.gradle
file generated:
plugins {
id 'org.springframework.boot' version '2.7.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.jos.dem.webclient'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 15
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-webflux')
implementation('org.springframework.boot:spring-boot-starter-tomcat')
compileOnly('org.projectlombok:lombok')
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test')
testImplementation('io.projectreactor:reactor-test')
}
NOTE: If you want to know what tools you need to have installed on your computer to create a Spring Boot basic project, please refer to my previous post: Spring Boot. Now add the latest Cucumber and Junit 5 Framework dependencies to your build.gradle
file:
testImplementation("info.cukes:cucumber-java:$cucumberVersion")
testImplementation("info.cukes:cucumber-junit:$cucumberVersion")
testImplementation("info.cukes:cucumber-spring:$cucumberVersion")
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
Next we are going to create a GET
request example using the GitHub API V3.
GET
Example: How to list public email addresses for a user.
Endpoint
GET /user/public_emails
Response
[
{
"email": "joseluis.delacruz@gmail.com",
"verified": true,
"primary": true,
"visibility": "public"
}
]
First, we are going to create our model definition:
package com.jos.dem.webclient.model;
import lombok.Data;
import lombok.AllArgsConstructor;
@Data
@AllArgsConstructor
public class PublicEmail {
private String email;
private boolean verified;
private boolean primary;
private String visibility;
}
Lombok is a great tool to avoid boilerplate code, for knowing more please go here. Next step is to create service definition:
package com.jos.dem.webclient.service;
import reactor.core.publisher.Flux;
import com.jos.dem.webclient.model.SSHKey;
import com.jos.dem.webclient.model.PublicEmail;
public interface UserService {
Flux<PublicEmail> getEmails();
}
Implementation:
package com.jos.dem.webclient.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import com.jos.dem.webclient.model.SSHKey;
import com.jos.dem.webclient.model.PublicEmail;
import com.jos.dem.webclient.service.UserService;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private WebClient webClient;
public Flux<PublicEmail> getEmails() {
return webClient.get().uri("user/public_emails").retrieve()
.bodyToFlux(PublicEmail.class);
}
}
WebClient
is a reactive client that provides an alternative to the RestTemplate
. It exposes a functional, fluent API and relies on non-blocking I/O which allows it to support high concurrency more efficiently. For knowing more, please go to my previous WebClient post: Spring Boot WebClient
package com.jos.dem.webclient;
import static java.nio.charset.StandardCharsets.UTF_8;
import org.springframework.util.Base64Utils;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
@SpringBootApplication
@PropertySource("classpath:application.properties")
public class WebClientApplication {
@Value("${github.api.url}")
private String githubApiUrl;
@Value("${username}")
private String username;
@Value("${token}")
private String token;
@Bean
public WebClient webClient() {
return WebClient
.builder()
.baseUrl(githubApiUrl)
.defaultHeader("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.build();
}
public static void main(String[] args) {
SpringApplication.run(WebClientApplication.class, args);
}
}
This project is using Github’s Basic Authentication and requires your Github username and access token, that you can generate from here: Personal Access Token. Once you have that token you need to provide it to our Spring Boot project; this time, we will use an application.properties
file, please go to the Project Configuration to get more information. The JUnit runner uses the JUnit framework to run the Cucumber Test. What we need is to create a single empty class with an annotation @RunWith(Cucumber.class) and define @CucumberOptions where we’re specifying the location of the Gherkin file, which is also known as the feature file:
package com.jos.dem.webclient;
import org.junit.runner.RunWith;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources")
public class CucumberTest {}
Gherkin is a DSL language used to describe an application feature that needs to be tested. Here is our person Gherkin feature definition file: src/test/resources/person.feature
Feature: As a user I can get my public emails
Scenario: User call to get his public emails
Then User gets his public emails
The next step is to create a class with a user service so we can call our get email endpoint:
package com.jos.dem.webclient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import com.jos.dem.webclient.model.PublicEmail;
import com.jos.dem.webclient.service.UserService;
import reactor.core.publisher.Flux;
@ContextConfiguration(classes = WebClientApplication.class)
@WebAppConfiguration
public class UserIntegrationTest {
@Autowired
private UserService userService;
Flux<PublicEmail> getEmails() throws Exception {
return userService.getEmails();
}
}
Now let’s create the method in the Java class to correspond to this test case scenario:
package com.jos.dem.webclient;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.jos.dem.webclient.model.PublicEmail;
import java.util.List;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import reactor.core.publisher.Flux;
public class UserGetTest extends UserIntegrationTest {
@Then("^User gets his public emails$")
public void shouldGetEmails() throws Exception {
List<PublicEmail> emails = getEmails()
.collectList()
.block();
PublicEmail email = emails.get(0);
assertTrue(emails.size() == 1, () -> "Should be 1 email");
assertAll("email",
() -> assertEquals("joseluis.delacruz@gmail.com", email.getEmail(), "Should contains josdem's email"),
() -> assertTrue(email.getVerified(), "Should be verified"),
() -> assertTrue(email.getPrimary(), "Should be primary"),
() -> assertEquals("public", email.getVisibility(), "Should be public")
);
}
}
POST
Example: Create a label
Endpoint
POST /repos/:owner/:repo/labels
Request
{
"name": "cucumber",
"description": "Cucumber is a very powerful testing framework written in the Ruby programming language",
"color": "ed14c5"
}
Response
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/josdem/webclient-workshop/labels/cucumber",
"name": "cucumber",
"description": "Cucumber is a very powerful testing framework written in the Ruby programming language",
"color": "ed14c5"
"default": true
}
Label model definition
package com.jos.dem.webclient.model;
import lombok.Data;
import lombok.AllArgsConstructor;
@Data
@AllArgsConstructor
public class Label {
private String name;
private String description;
private String color;
}
Label service definition
package com.jos.dem.webclient.service;
import reactor.core.publisher.Mono;
import com.jos.dem.webclient.model.LabelResponse;
import org.springframework.web.reactive.function.client.ClientResponse;
public interface LabelService {
Mono<LabelResponse> create();
}
Label service implementation
package com.jos.dem.webclient.service.impl;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import com.jos.dem.webclient.model.Label;
import com.jos.dem.webclient.model.LabelResponse;
import com.jos.dem.webclient.service.LabelService;
import com.jos.dem.webclient.util.LabelCreator;
@Service
public class LabelServiceImpl implements LabelService {
@Autowired
private WebClient webClient;
@Autowired
private LabelCreator labelCreator;
@Value("${github.labels.path}")
private String githubLabelsPath;
public Mono<LabelResponse> create() {
return webClient.post()
.uri(githubLabelsPath).accept(APPLICATION_JSON)
.body(Mono.just(labelCreator.create()), Label.class)
.retrieve()
.bodyToMono(LabelResponse.class);
}
}
Label creator is just a collaboratior helping to create label model and fill data on it, now let see create lable scenario definition.
Feature: As a user I want to create a label
Scenario: User call to create new label with cucumer as a name
Then User creates a new label
In the test side, we are going to create a label integration definition:
package com.jos.dem.webclient;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.reactive.function.client.ClientResponse;
import com.jos.dem.webclient.model.LabelResponse;
import com.jos.dem.webclient.service.LabelService;
import reactor.core.publisher.Mono;
@ContextConfiguration(classes = WebClientApplication.class)
@WebAppConfiguration
public class LabelIntegrationTest {
@Autowired
private LabelService labelService;
Mono<LabelResponse> create() throws Exception {
return labelService.create();
}
}
This is the Junit 5 test implementation
package com.jos.dem.webclient;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.jos.dem.webclient.model.LabelResponse;
import cucumber.api.java.en.Then;
public class LabelPostTest extends LabelIntegrationTest {
private LabelResponse response;
@Then("^User creates a new label$")
public void shouldCreateLabel() throws Exception {
LabelResponse response = create()
.block();
assertAll("response",
() -> assertEquals("cucumber", response.getName()),
() -> assertEquals("ed14c5", response.getColor())
);
}
}
PATCH
Example: Update a label
Endpoint
PATCH /repos/:owner/:repo/labels/:current_name
Request
{
"name": "spock",
"description": "Spock is a testing and specification framework for Java and Groovy applications. It is beautiful and highly expressive",
"color": "ff0000"
}
Response
Status: 200 OK
Label service definition updated:
package com.jos.dem.webclient.service;
import reactor.core.publisher.Mono;
import com.jos.dem.webclient.model.LabelResponse;
import org.springframework.web.reactive.function.client.ClientResponse;
public interface LabelService {
Mono<LabelResponse> create();
Mono<ClientResponse> update(String name);
}
Label service implementation updated:
package com.jos.dem.webclient.service.impl;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import com.jos.dem.webclient.model.Label;
import com.jos.dem.webclient.model.LabelResponse;
import com.jos.dem.webclient.service.LabelService;
import com.jos.dem.webclient.util.LabelCreator;
@Service
public class LabelServiceImpl implements LabelService {
@Autowired
private WebClient webClient;
@Autowired
private LabelCreator labelCreator;
@Value("${github.labels.path}")
private String githubLabelsPath;
public Mono<LabelResponse> create() {
return webClient.post()
.uri(githubLabelsPath).accept(APPLICATION_JSON)
.body(Mono.just(labelCreator.create()), Label.class)
.retrieve()
.bodyToMono(LabelResponse.class);
}
public Mono<ClientResponse> update(String name){
return webClient.patch()
.uri(githubLabelsPath + "/" + name).accept(APPLICATION_JSON)
.body(Mono.just(labelCreator.update()), Label.class)
.exchange();
}
}
Feature definition updated:
Feature: As a user I want to create a label
Scenario: User call to create new label with cucumer as a name
Then User creates a new label
Scenario: User call to update label to spock as a name
Then User updates label
Label test integration client definition updated:
package com.jos.dem.webclient;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.reactive.function.client.ClientResponse;
import com.jos.dem.webclient.model.LabelResponse;
import com.jos.dem.webclient.service.LabelService;
import reactor.core.publisher.Mono;
@ContextConfiguration(classes = WebClientApplication.class)
@WebAppConfiguration
public class LabelIntegrationTest {
@Autowired
private LabelService labelService;
Mono<LabelResponse> create() throws Exception {
return labelService.create();
}
Mono<ClientResponse> update(String name) throws Exception {
return labelService.update(name);
}
}
This is the Junit 5 test implementation updated:
package com.jos.dem.webclient;
import static org.springframework.http.HttpStatus.OK;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.springframework.web.reactive.function.client.ClientResponse;
import java.util.List;
import cucumber.api.java.en.Then;
public class LabelUpdateTest extends LabelIntegrationTest {
@Then("^User updates label$")
public void shouldCreateLabel() throws Exception {
ClientResponse response = update("cucumber")
.block();
assertEquals(OK, response.statusCode(), "Should update to spock information");
}
}
DELETE
Example: Delete a label
Endpoint
DELETE /repos/:owner/:repo/labels/:name
Response
Status: 204 No Content
Label service definition updated:
package com.jos.dem.webclient.service;
import reactor.core.publisher.Mono;
import com.jos.dem.webclient.model.LabelResponse;
import org.springframework.web.reactive.function.client.ClientResponse;
public interface LabelService {
Mono<LabelResponse> create();
Mono<ClientResponse> update(String name);
Mono<ClientResponse> delete(String name);
}
Label service implementation updated:
package com.jos.dem.webclient.service.impl;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import com.jos.dem.webclient.model.Label;
import com.jos.dem.webclient.model.LabelResponse;
import com.jos.dem.webclient.service.LabelService;
import com.jos.dem.webclient.util.LabelCreator;
@Service
public class LabelServiceImpl implements LabelService {
@Autowired
private WebClient webClient;
@Autowired
private LabelCreator labelCreator;
@Value("${github.labels.path}")
private String githubLabelsPath;
public Mono<LabelResponse> create() {
return webClient.post()
.uri(githubLabelsPath).accept(APPLICATION_JSON)
.body(Mono.just(labelCreator.create()), Label.class)
.retrieve()
.bodyToMono(LabelResponse.class);
}
public Mono<ClientResponse> update(String name){
return webClient.patch()
.uri(githubLabelsPath + "/" + name).accept(APPLICATION_JSON)
.body(Mono.just(labelCreator.update()), Label.class)
.exchange();
}
public Mono<ClientResponse> delete(String name){
return webClient.delete()
.uri(githubLabelsPath + "/" + name).accept(APPLICATION_JSON)
.exchange();
}
}
Feature definition updated:
Feature: As a user I want to create a label
Scenario: User call to create new label with cucumer as a name
Then User creates a new label
Scenario: User call to update label to spock as a name
Then User updates label
Scenario: User call to delete a label spock as a name
Then User deletes label
Label test integration client definition updated:
package com.jos.dem.webclient;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.reactive.function.client.ClientResponse;
import com.jos.dem.webclient.model.LabelResponse;
import com.jos.dem.webclient.service.LabelService;
import reactor.core.publisher.Mono;
@ContextConfiguration(classes = WebClientApplication.class)
@WebAppConfiguration
public class LabelIntegrationTest {
@Autowired
private LabelService labelService;
Mono<LabelResponse> create() throws Exception {
return labelService.create();
}
Mono<ClientResponse> update(String name) throws Exception {
return labelService.update(name);
}
Mono<ClientResponse> delete(String name) throws Exception {
return labelService.delete(name);
}
}
This is the Junit 5 test implementation updated:
package com.jos.dem.webclient.service.impl;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import com.jos.dem.webclient.model.Label;
import com.jos.dem.webclient.model.LabelResponse;
import com.jos.dem.webclient.service.LabelService;
import com.jos.dem.webclient.util.LabelCreator;
@Service
public class LabelServiceImpl implements LabelService {
@Autowired
private WebClient webClient;
@Autowired
private LabelCreator labelCreator;
@Value("${github.labels.path}")
private String githubLabelsPath;
public Mono<LabelResponse> create() {
return webClient.post()
.uri(githubLabelsPath).accept(APPLICATION_JSON)
.body(Mono.just(labelCreator.create()), Label.class)
.retrieve()
.bodyToMono(LabelResponse.class);
}
public Mono<ClientResponse> update(String name){
return webClient.patch()
.uri(githubLabelsPath + "/" + name).accept(APPLICATION_JSON)
.body(Mono.just(labelCreator.update()), Label.class)
.exchange();
}
public Mono<ClientResponse> delete(String name){
return webClient.delete()
.uri(githubLabelsPath + "/" + name).accept(APPLICATION_JSON)
.exchange();
}
}
Important: Regarding to ordering feature test execution Cucumber features run in alphabetical order by feature file name.
Using Maven
You can do the same using Maven, the only difference is that you need to specify –build=maven parameter in the spring init command line:
spring init --dependencies=webflux,lombok --build=maven --language=java spring-boot-web-client
This is the pom.xml
file generated along with Cucumber and Junit as dependencies on it added manualy:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jos.dem</groupId>
<artifactId>webclient</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>webclient-workshop</name>
<description>Shows how to integrate Cucumber to your Spring Boot application</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<cucumber.version>1.2.5</cucumber.version>
<junit.jupiter.version>5.3.1</junit.jupiter.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-spring</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
To browse the project go here, to download the project:
git clone https://github.com/josdem/webclient-workshop.git
To run the project with Gradle:
gradle test
To run the project with Maven:
mvn test