In this post, I will describe Spring Restdocs and Spring Cloud Contract integration into Cucumber tests with Spring Webflux. The main problem is that we can’t use the most of common JUnit and Spring Test annotations like @Before, @After and @Rule in Cucumber tests, so we have to set up testing environment manually. This post is a webflux adaptation of the previous post.
Project dependencies
Our test project will be a secured rest service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </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>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-verifier</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-wiremock</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-webtestclient</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.2.5</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-spring</artifactId> <version>1.2.5</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.2.5</version> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.M3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> |
Cucumber feature and runner
We will develop a service providing contacts list. We can describe it in this feature:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# language: en Feature: contacts list Scenario: client requests contacts list Given contacts stored in the storage: | email | name | | jack.daniels@example.com | Jack Daniels | | jim.beam@example.com | Jim Beam | | john.dewar@example.com | John Dewar | When client sends GET "/contacts" Then service should return response with status 200 Then response body should be compatible with "application/json" Then response body should be an array and contain 3 entries and should be documented as "findAll" |
In our next step we need to create a class running our Cucumber tests as usual:
1 2 3 4 5 6 7 |
@RunWith(Cucumber.class) @CucumberOptions( strict = true, glue = "name.alexkosarev.sandbox.web.handlers.contacts", features = "classpath:features/contacts/FindAll.feature") public class FindAllTest { } |
- With annotation @RunWith we specify runner class that will be used in our tests
- With annotation @CucumberOptions we specify Cucumber options:
- Property strict defines implementation mandatory for all steps definitions
- Property glue defines a package containing classes with steps definitions implementation
- Property features defines a location containing Cucumber tests features
If we try to run our tests right now we’ll get output like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
Undefined scenarios: features/contacts/FindAll.feature:4 # Scenario: client requests contacts list 1 Scenarios (1 undefined) 5 Steps (5 undefined) 0m0,048s You can implement missing steps with the snippets below: @Given("^contacts stored in the storage:$") public void contacts_stored_in_the_storage(DataTable arg1) throws Throwable { // Write code here that turns the phrase above into concrete actions // For automatic transformation, change DataTable to one of // List<YourType>, List<List<E>>, List<Map<K,V>> or Map<K,V>. // E,K,V must be a scalar (String, Integer, Date, enum etc) throw new PendingException(); } @When("^client sends GET \"([^\"]*)\"$") public void client_sends_GET(String arg1) throws Throwable { // Write code here that turns the phrase above into concrete actions throw new PendingException(); } @Then("^service should return response with status (\\d+)$") public void service_should_return_response_with_status(int arg1) throws Throwable { // Write code here that turns the phrase above into concrete actions throw new PendingException(); } @Then("^response body should be compatible with \"([^\"]*)\"$") public void response_body_should_be_compatible_with(String arg1) throws Throwable { // Write code here that turns the phrase above into concrete actions throw new PendingException(); } @Then("^response body should be an array and contain (\\d+) entries and should be documented as \"([^\"]*)\"$") public void response_body_should_be_an_array_and_contain_entries_and_should_be_documented_as(int arg1, String arg2) throws Throwable { // Write code here that turns the phrase above into concrete actions throw new PendingException(); } |
So we need to implement listed methods. But first of all we should integrate Spring Test into our Cucumber tests.
Spring Test integration and steps definitions implementation
We need a class initializing Spring application context and configuring Spring Restdocs and Spring Cloud Contract.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@SpringBootTest @ContextConfiguration(classes = Spring5CucumberTestsApplication.class) public abstract class Spring5CucumberIntegrationTest { @Autowired private ApplicationContext applicationContext; @Autowired @MockBean ContactRepository contactRepository; WebTestClient webTestClient; ManualRestDocumentation manualRestDocumentation; public void setUp(Class testClass, String testMethod, String outputDirectory) { manualRestDocumentation = new ManualRestDocumentation(outputDirectory); manualRestDocumentation.beforeTest(testClass, testMethod); webTestClient = WebTestClient.bindToApplicationContext(applicationContext) // Applying Spring Security .apply(springSecurity()) // Applying Spring Restdocs and Spring Cloud Contract .configureClient().baseUrl("http://api.example.com")// Spring Restdocs' document() will fail if baseUrl isn't set .filter(documentationConfiguration(manualRestDocumentation).snippets().withAdditionalDefaults(new WireMockSnippet())) .build(); } public void tearDown() { manualRestDocumentation.afterTest(); } } |
Note than we cannot use Cucumber’s @Before and @After annotations in this class. But we can do it in classes with steps definitions implementation.
So now we can implement step definitions in a class extending SpringCucumberIntegrationTest:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class FindAllStepDefs extends Spring5CucumberIntegrationTest { private WebTestClient.ResponseSpec exchange; @Before public void setUp() { super.setUp(FindAllTest.class, "setUp", "target/generated-snippets/contacts"); } @After public void tearDown() { super.tearDown(); } @Given("^contacts stored in the storage:$") public void contacts_stored_in_the_storage(List<Contact> contacts) throws Throwable { doReturn(fromIterable(contacts)).when(contactRepository) .findAll(); } @When("^client sends GET \"([^\"]*)\"$") public void client_sends_GET(String path) throws Throwable { exchange = webTestClient.mutateWith(mockUser()).get().uri(path).exchange(); } @Then("^service should return response with status (\\d+)$") public void service_should_return_response_with_status(int status) throws Throwable { exchange.expectStatus().isEqualTo(status); } @Then("^response body should be compatible with \"([^\"]*)\"$") public void response_body_should_be_compatible_with(String contentType) throws Throwable { exchange.expectHeader().contentType(MediaType.parseMediaType(contentType)); } @Then("^response body should be an array and contain (\\d+) entries and should be documented as \"([^\"]*)\"$") public void response_body_should_be_an_array_and_contain_entries(int entriesCount, String documentationIdentifier) throws Throwable { exchange.expectBody() .jsonPath("$").isArray() .jsonPath("$.length()").isEqualTo(entriesCount) .consumeWith(document(documentationIdentifier)); } } |
Implementation
We should implement routing and handler for our tests:
1 2 3 4 5 6 7 8 9 |
public class ContactsHandler { private final ContactRepository contactRepository; public Mono<ServerResponse> getAllContacts(ServerRequest serverRequest) { return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON) .body(contactRepository.findAll(), Contact.class); } } |
Pay your attention that ContactRepository is a subtype of ReactiveCrudRepository.
1 2 3 4 5 6 7 |
public class RouterConfig { @Bean public RouterFunction<ServerResponse> routerFunction(ContactsHandler contactsHandler) { return route(GET("/contacts"), contactsHandler::getAllContacts); } } |
We need to enable webflux support in our application:
1 2 3 4 5 6 7 8 |
@EnableWebFlux @SpringBootApplication public class Spring5CucumberTestsApplication { public static void main(String[] args) { SpringApplication.run(Spring5CucumberTestsApplication.class, args); } } |