综合技术

Spring Boot REST Service Integration Testing Using Cucumber

微信扫一扫,分享到朋友圈

Spring Boot REST Service Integration Testing Using Cucumber
0

In this article, we are going to learn how to write an integration test for REST services using Cucumber with Spring Boot.

What Is Cucumber?

Cucumber is a tool that supports Behavior-Driven Development(BDD) .

Cucumber reads executable specifications written in Plain Text and validates that your application behaves as what is mentioned in the specification. The specification consists of multiple examples or scenarios. For example:

Feature: Bag Functionality

Scenario: Putting one thing in the bag
Given the bag is empty
When I put 1 potato in the bag
Then the bag should contain only 1 potato

Each scenario is a list of steps for Cucumber to work through. Cucumber verifies that the software conforms with the specification and generates a report indicating :white_check_mark: success or :x: failure for each scenario.

In order for Cucumber to understand the scenarios, they must follow some basic syntax rules, called Gherkin .

What Is Gherkin?

Gherkin is a simple set of grammar rules that make Plain Text structured enough for Cucumber to understand. The scenario above is written in Gherkin.

Test after is not BDD. Many people write tests after the code is written, even with Cucumber. This is not BDD or TDD, because the tests do not drive the implementation when they are written afterwards.

Many people write tests after the code is written, even with Cucumber. This is not BDD or TDD, because tests do not derive from the implementation when they are written afterward.

Gherkin serves multiple purposes:

  • Unambiguous executable specification
  • Automated testing using Cucumber
  • Document how the system actually behaves

Gherkin documents are stored in .feature text files and are typically versioned in source control alongside the software.

Step Definitions

In addition to feature files , Cucumber needs a set of step definitions . Step definitions map (or “glue”) each Gherkin step to programming code to carry out the action that should be performed by the step.

Step definitions hard-wire the specification to the implementation. Additionally, step definitions can be written in many programming languages. Here is an example using Java:

When("I put {int} {word} in the bag", (Integer quantity, String something) -> {
IntStream.range(0, quantity)
            .peek(n -> log.info("Putting a {} in the bag, number {}", something, quantity))
.map(ignore -> put(something))
.forEach(statusCode -> assertThat(statusCode)
                    .isEqualTo(HttpStatus.CREATED.value()));

});

The steps can be parameterized using arguments. We can reuse the same step definition in multiple scenarios with different values, or we can also pass a data table to the same scenario. If we pass a data table, each row is called example .

Project Structure

Let’s build the project from scratch, just go to https://start.spring.io/ and select spring 2.2.0 with Java 8, Maven, and web dependency, and then, click on "generate project." Import the project in Eclipse. The code for this project is available at my GitHub repository

The E2E project structure is shown in the picture.

Cucumber Dependencies

As we are going to use the lambda expressions API (Java 8) to write the step definitions, add the following dependency to your pom.xml :

<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>

Also, we are using Cucumber with our Spring Boot application, so we would like to load all the web application context including our configured beans. Therefore, we need to add the dependency cucumber-spring to our project apart from Cucumber’s basic on. As ${cucumber.version}, I have used 4.2.6 (configured in the same pom.xml).

Entry Point: @RunWith Cucumber

Let’s have a simple class to configure the test; it’s mandatory to have @RunWith(Cucumber.class) .

package com.manoj.training.app;

import org.junit.runner.RunWith;

import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;

@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources/features/bag.feature", plugin = {"pretty", "html:target/cucumber"})
public class BagCucmberIntegrationTest {

}

The @CucumberOptions annotation is responsible for two tasks here: pointing right file feature file and configuring the plugin for better reporting of tests both in the console output and as HTML. The second part ( plugin ) is optional.

Here is our sample feature file for this project. We have two scenarios for the bag functionality feature.

Feature: Bag Functionality

Scenario: Putting one thing in the bag
Given the bag is empty
When I put 1 potato in the bag
Then the bag should contain only 1 potato

Scenario: Putting few things in the bag
Given the bag is empty
When I put 1 potato in the bag
And I put 2 cucumber in the bag
Then the bag should contain 1 potato
And the bag should contain 2 cucumber

Base Class for Cucumber Steps

As we are using Cucumber with Spring Boot, Cucumber does not load the Spring Context, so we need to implement the base class which loads the Spring-Boot-specific configurations.

package com.manoj.training.app;

import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Ignore
public class SpringCucumberIntegrationTests {

private final String SERVER_URL = "http://localhost";
private final String THINGS_ENDPOINT = "/things";

private RestTemplate restTemplate;

@LocalServerPort
protected int port;

public SpringCucumberIntegrationTests() {

this.restTemplate = new RestTemplate();
}

private String thingsEndpoint() {
return SERVER_URL + ":" + port + THINGS_ENDPOINT;
}

int put(String something) {
return restTemplate
          .postForEntity(thingsEndpoint(), something, Void.class)
          .getStatusCodeValue();
}

Bag getContents() {
return restTemplate
          .getForEntity(thingsEndpoint(), Bag.class)
          .getBody();
}

void clean() {
restTemplate.delete(thingsEndpoint());
}
}

Here, this class serves two purposes:

  • The main purpose of this class is to configure @RunWith(SpringRunner.class) ; this annotation, together with @SpringBootTest , will load the web application context. Since we want to run this test on a real server and not use mockMvc, we need to use either WebEnvironment.RANDOM_PORT.
  • The REST logic is to provide the common functionality to our test. I created  restTemplate  and generic methods to use the application functionalities. The random ports get injected into the class as configured with @LocalServerPort annotations.

Step Definition Class

In this part, we need to connect our Cucumber test class with Spring Boot Context: the step definition class, which must extend the annotated class that we created above.

package com.manoj.training.app;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;

import cucumber.api.java8.En;

public class BagCucmberStepDefinitions extends SpringCucumberIntegrationTests implements En {
private final Logger log = LoggerFactory.getLogger(BagCucmberStepDefinitions.class);

public BagCucmberStepDefinitions() {

Given("the bag is empty", () -> {
clean();
assertThat(getContents().isEmpty()).isTrue();
});

When("I put {int} {word} in the bag", (Integer quantity, String something) -> {
IntStream.range(0, quantity)
              .peek(n -> log.info("Putting a {} in the bag, number {}", something, quantity))
.map(ignore -> put(something))
.forEach(statusCode -> assertThat(statusCode)
                    .isEqualTo(HttpStatus.CREATED.value()));

});

Then("the bag should contain only {int} {word}", (Integer quantity, String something) -> {
assertNumberOfTimes(quantity, something, true);
});

Then("the bag should contain {int} {word}", (Integer quantity, String something) -> {
assertNumberOfTimes(quantity, something, false);
});

}

private void assertNumberOfTimes(final int quantity, final String something, final boolean onlyThat) {
final List<String> things = getContents().getThings();
log.info("Expecting {} times {}. The bag contains {}", quantity, something, things);
final int timesInList = Collections.frequency(things, something);
assertThat(timesInList).isEqualTo(quantity);
if (onlyThat) {
assertThat(timesInList).isEqualTo(things.size());
}
}

}

Therefore, the key to making this work is that these step definition classes must extend a class annotated with @SpringBootTest.

Running the Application

You can run the tests from Eclipse or using Maven.

$mvn clean test

Below, you can see the console log with test execution summary:

2019-04-14 13:52:41.595  INFO 1424 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.manoj.training.app.SpringCucumberExampleApplication for test class com.manoj.training.app.BagCucmberStepDefinitions
2019-04-14 13:52:41.607  INFO 1424 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]
2019-04-14 13:52:41.633  INFO 1424 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Using TestExecutionListeners: [or[email protected]78e59b04, org.springframework.test[email protected]2e6f1f48, org.spri[email protected]5ed88e31, org.springframework.boot.test.a[email protected]574413bd, org.springfra[email protected]3ad9fea, org.springf[email protected]556e4588, org.springframework[email protected]4f5df012, org.springframework.boot.test.autoconfi[email protected]38f3dbbf, org.springframework.boot.test.autoconfi[email protected]574e4184, org.springframework.boo[email protected]70ab1ce0]
2019-04-14 13:52:41.672  INFO 1424 --- [           main] c.m.t.app.BagCucmberStepDefinitions      : Putting a potato in the bag, number 1
2019-04-14 13:52:41.688  INFO 1424 --- [           main] c.m.t.app.BagCucmberStepDefinitions      : Putting a cucumber in the bag, number 2
2019-04-14 13:52:41.709  INFO 1424 --- [           main] c.m.t.app.BagCucmberStepDefinitions      : Putting a cucumber in the bag, number 2
2019-04-14 13:52:41.726  INFO 1424 --- [           main] c.m.t.app.BagCucmberStepDefinitions      : Expecting 1 times potato. The bag contains [potato, cucumber, cucumber]
2019-04-14 13:52:41.735  INFO 1424 --- [           main] c.m.t.app.BagCucmberStepDefinitions      : Expecting 2 times cucumber. The bag contains [potato, cucumber, cucumber]
Feature: Bag Functionality

  Scenario: Putting one thing in the bag      # src/test/resources/features/bag.feature:3
    Given the bag is empty                    # BagCucmberStepDefinitions.java:20
    When I put 1 potato in the bag            # BagCucmberStepDefinitions.java:25
    Then the bag should contain only 1 potato # BagCucmberStepDefinitions.java:32

  Scenario: Putting few things in the bag # src/test/resources/features/bag.feature:8
    Given the bag is empty                # BagCucmberStepDefinitions.java:20
    When I put 1 potato in the bag        # BagCucmberStepDefinitions.java:25
    And I put 2 cucumber in the bag       # BagCucmberStepDefinitions.java:25
    Then the bag should contain 1 potato  # BagCucmberStepDefinitions.java:36
    And the bag should contain 2 cucumber # BagCucmberStepDefinitions.java:36

2 Scenarios (2 passed)
8 Steps (8 passed)
0m12.234s

[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.785 s - in com.manoj.training.app.BagCucmberIntegrationTest
[INFO] Running com.manoj.training.app.SpringCucumberIntegrationTests
[WARNING] Tests run: 1, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.004 s - in com.manoj.training.app.SpringCucumberIntegrationTests
[INFO] 
[INFO] Results:
[INFO] 
[WARNING] Tests run: 3, Failures: 0, Errors: 0, Skipped: 1

Also, the test execution report can be found in target/cucumber/index.html as shown below:

That will only happen if you set the plugin “pretty” in @CucumberOptions (like we did in this example). Since we also set the option to generate HTML reports, we’ll have them available in the target/cucumber folder. Don’t expect a fancy report, it’s very simple – yet functional.

Conclusion

So, in this post, we have learned how to use Cucumber to perform integration testing of the REST API with Spring Boot. If you have anything that you want to add or share, then please share it in the comments below.

阅读原文...

微信扫一扫,分享到朋友圈

Spring Boot REST Service Integration Testing Using Cucumber
0
DZone

python接口自动化(十七)--Json 数据处理---一次爬坑记(详解)

上一篇

Role of Governance to Make Low-Code Platforms More Productive

下一篇

评论已经被关闭。

插入图片

热门分类

往期推荐

Spring Boot REST Service Integration Testing Using Cucumber

长按储存图像,分享给朋友