In this post, I will describe Spring Restdocs and Spring Cloud Contract integration into Cucumber tests. 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.
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 |
<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-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</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> <!-- Spring Security Test --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Cloud Contract --> <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> <!-- Spring Restdocs --> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <scope>test</scope> </dependency> <!-- Cucumber --> <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> |
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 14 |
# 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 Then request and response should be logged Then request and response should be documented |
In our next step we need to create a class running our Cucumber tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package name.alexkosarev.sandbox.web.controllers.contacts; import cucumber.api.CucumberOptions; import cucumber.api.junit.Cucumber; import org.junit.runner.RunWith; @RunWith(Cucumber.class) @CucumberOptions( strict = true, glue = "name.alexkosarev.sandbox.web.controllers.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 43 44 45 46 47 48 49 50 51 52 53 54 |
Undefined scenarios: features/contacts/FindAll.feature:3 # Scenario: client requests contacts list 1 Scenarios (1 undefined) 7 Steps (7 undefined) 0m0,000s 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$") public void response_body_should_be_an_array_and_contain_entries(int arg1) throws Throwable { // Write code here that turns the phrase above into concrete actions throw new PendingException(); } @Then("^request and response should be logged$") public void request_and_response_should_be_logged() throws Throwable { // Write code here that turns the phrase above into concrete actions throw new PendingException(); } @Then("^request and response should be documented$") public void request_and_response_should_be_documented() 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 33 34 |
@SpringBootTest @ContextConfiguration(classes = SandboxTestingApplication.class) public abstract class SpringCucumberIntegrationTest { @MockBean ContactRepository contactRepository; @Autowired private WebApplicationContext webApplicationContext; MockMvc mockMvc; private ManualRestDocumentation restDocumentation; public void setUp() { // Spring Restdocs restDocumentation = new ManualRestDocumentation("target/generated-snippets"); // MockMVC mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(springSecurity())// applying Spring Security .apply(documentationConfiguration(restDocumentation)// applying Spring Restdocs .snippets().withAdditionalDefaults(new WireMockSnippet()))// applying Spring Cloud Contract и WireMock .build(); // Spring Restdocs context initialization restDocumentation.beforeTest(FindAllTest.class, "setUp"); } public void tearDown() { // Spring Restdocs context shutdown restDocumentation.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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public class FindAllStepDefs extends SpringCucumberIntegrationTest { private ResultActions resultActions; @Before public void setUp() { super.setUp(); } @Given("^contacts stored in the storage:$") public void contacts_stored_in_the_storage(List<Contact> contacts) throws Throwable { doReturn(contacts).when(contactRepository) .findAll(); } @When("^client sends GET \"([^\"]*)\"$") public void client_sends_GET(String requestPath) throws Throwable { resultActions = mockMvc.perform(get(requestPath).with(user("tester"))); } @Then("^service should return response with status (\\d+)$") public void service_should_return_response_with_status(int status) throws Throwable { resultActions.andExpect(status().is(status)); } @Then("^response body should be compatible with \"([^\"]*)\"$") public void response_body_should_be_compatible_with(String contentType) throws Throwable { resultActions.andExpect(content().contentTypeCompatibleWith(contentType)); } @Then("^response body should be an array and contain (\\d+) entries$") public void response_body_should_be_an_array_and_contain_entries(int entriesCount) throws Throwable { resultActions.andExpect(jsonPath("$.length()").value(3)); } @Then("^request and response should be logged$") public void request_and_response_should_be_logged() throws Throwable { resultActions.andDo(print()); } // This method will generate Restdocs and WireMock snippets and stubs @Then("^request and response should be documented$") public void request_and_response_should_be_documented() throws Throwable { resultActions.andDo(document("contacts/findAll", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), responseFields( fieldWithPath("[0].email").description("Contact email"), fieldWithPath("[0].name").description("Contact name") ) )); } @After public void tearDown() { super.tearDown(); } } |
And implementation of controller satisfying our tests will be:
1 2 3 4 5 6 7 8 9 10 11 12 |
@RestController @RequestMapping("contacts") @RequiredArgsConstructor public class ContactsController { private final ContactRepository repository; @GetMapping public ResponseEntity<List<Contact>> findAll() { return ResponseEntity.ok((List<Contact>) repository.findAll()); } } |
Additional resources
- Project source code on GitHub
- Spring Restdocs documentation
- Spring Cloud Contract documentation
- Cucumber and Gherkin documentation