Cucumber is a tool for behavior-driven development (BDD): you describe how a feature should behave in plain language (Gherkin), and Java „step definitions“ turn that language into executable test code. This post walks through a minimal but realistic setup: a Spring Boot 4 service with default, dev, and mock profiles, a REST client configured entirely from application.yml, WireMock standing in for a downstream dependency only in the mock profile, one Cucumber scenario, and one plain test that’s guaranteed to run no matter which profile is active.
1. Dependencies
xml
<properties> <java.version>17</java.version> <cucumber.version>7.20.1</cucumber.version> <wiremock.version>3.9.2</wiremock.version></properties><dependencies> <!-- Spring Boot 4 renamed the web starter to "webmvc" --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- Test starters --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency> <dependency> <!-- provides RestTestClient autoconfiguration --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-restclient-test</artifactId> <scope>test</scope> </dependency> <!-- Cucumber --> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-java</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> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-junit-platform-engine</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-suite</artifactId> <scope>test</scope> </dependency> <!-- WireMock, only ever touched by the mock profile --> <dependency> <groupId>org.wiremock</groupId> <artifactId>wiremock-standalone</artifactId> <version>${wiremock.version}</version> <scope>test</scope> </dependency></dependencies>
Check Maven Central for the latest patch versions of Cucumber and WireMock before you pin these.
3. The minimal application
The REST client’s base URL and timeouts come entirely from configuration, so the same WeatherClient code talks to a real provider in dev, a placeholder in default, and WireMock in mock.
java
// config/WeatherApiProperties.java@ConfigurationProperties(prefix = "weather.api")public record WeatherApiProperties(String baseUrl, Duration connectTimeout, Duration readTimeout) {}
java
// config/WeatherClientConfig.java@Configuration@EnableConfigurationProperties(WeatherApiProperties.class)public class WeatherClientConfig { @Bean RestClient weatherRestClient(WeatherApiProperties props) { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(props.connectTimeout()); factory.setReadTimeout(props.readTimeout()); return RestClient.builder() .baseUrl(props.baseUrl()) .requestFactory(factory) .build(); }}
java
// WeatherForecast.javapublic record WeatherForecast(String city, double temperature, String condition) {}
java
// WeatherClient.java@Servicepublic class WeatherClient { private final RestClient restClient; public WeatherClient(RestClient weatherRestClient) { this.restClient = weatherRestClient; } public WeatherForecast getForecast(String city) { return restClient.get() .uri("/forecast/{city}", city) .retrieve() .body(WeatherForecast.class); }}
java
// WeatherController.java@RestController@RequestMapping("/api/weather")public class WeatherController { private final WeatherClient weatherClient; public WeatherController(WeatherClient weatherClient) { this.weatherClient = weatherClient; } @GetMapping("/{city}") public WeatherForecast forecast(@PathVariable String city) { return weatherClient.getForecast(city); }}
java
// WeatherApplication.java@SpringBootApplicationpublic class WeatherApplication { public static void main(String[] args) { SpringApplication.run(WeatherApplication.class, args); }}
4. application.yml for default, dev, and mock
application.yml holds defaults shared by every profile, plus the REST client settings for whatever you consider the „baseline“ environment:
yaml
# application.ymlspring: application: name: weather-serviceweather: api: base-url: https://api.weather-provider.dabbaghi.com connect-timeout: 2s read-timeout: 3smanagement: endpoints: web: exposure: include: health,info
application-dev.yml overrides only what differs — a different host and more relaxed timeouts for a slower environment:
yaml
# application-dev.ymlweather: api: base-url: https://dev.weather-provider.dabbaghi.com connect-timeout: 3s read-timeout: 5slogging: level: com.dabbaghi.weather: DEBUG
application-mock.yml points the same client at a local WireMock instance, on a fixed port so the test setup can target it deterministically:
yaml
# application-mock.ymlweather: api: base-url: http://localhost:9561 connect-timeout: 1s read-timeout: 2swiremock: server: port: 9561
Notice that WeatherClient never knows which profile is active — it only ever reads weather.api.base-url, connect-timeout, and read-timeout.
5. WireMock, active only for the mock profile
This configuration is annotated @Profile("mock"), so Spring only creates the WireMock server bean — and only starts the stub — when mock is the active profile. Under default or dev, this class is never instantiated.
java
// wiremock/WireMockMockProfileConfig.java@TestConfiguration@Profile("mock")public class WireMockMockProfileConfig { @Value("${wiremock.server.port:9561}") private int port; private WireMockServer wireMockServer; @PostConstruct void startAndStub() { wireMockServer = new WireMockServer(WireMockConfiguration.options().port(port)); wireMockServer.start(); WireMock.configureFor(port); wireMockServer.stubFor(get(urlEqualTo("/forecast/Paris")) .willReturn(okJson(""" {"city": "Paris", "temperature": 18.5, "condition": "Sunny"} """))); } @PreDestroy void stop() { if (wireMockServer != null) { wireMockServer.stop(); } }}
6. The feature file
gherkin
# src/test/resources/features/weather.featureFeature: Weather forecast API Scenario: Get the current forecast for a known city Given the weather provider has a forecast for "Paris" When I request the weather forecast for "Paris" Then the temperature should be 18.5 degrees And the condition should be "Sunny"
7. Wiring Cucumber to Spring
cucumber-spring needs exactly one class annotated @CucumberContextConfiguration. This is also where the WireMock configuration gets imported — note there’s no @ActiveProfiles here, so the profile is decided entirely by whatever you pass at runtime.
java
// cucumber/CucumberSpringConfiguration.java@CucumberContextConfiguration@SpringBootTest(classes = WeatherApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@AutoConfigureRestTestClient@Import(WireMockMockProfileConfig.class)public class CucumberSpringConfiguration {}
The step definitions use RestTestClient, the fluent HTTP testing client introduced in Spring Framework 7 / Spring Boot 4 (it replaces TestRestTemplate for new code):
java
// cucumber/WeatherSteps.javapublic class WeatherSteps { @Autowired private RestTestClient restTestClient; private WeatherForecast forecast; @Given("the weather provider has a forecast for {string}") public void theProviderHasAForecastFor(String city) { // The stub is already registered by WireMockMockProfileConfig when the // mock profile is active. Nothing else to set up for this scenario. } @When("I request the weather forecast for {string}") public void iRequestTheForecastFor(String city) { forecast = restTestClient.get() .uri("/api/weather/{city}", city) .exchange() .expectStatus().isOk() .expectBody(WeatherForecast.class) .returnResult() .getResponseBody(); } @Then("the temperature should be {double} degrees") public void theTemperatureShouldBe(double expected) { assertThat(forecast.temperature()).isEqualTo(expected); } @Then("the condition should be {string}") public void theConditionShouldBe(String expected) { assertThat(forecast.condition()).isEqualTo(expected); }}
And the JUnit 5 suite that actually discovers and runs the .feature files:
java
// cucumber/RunCucumberTest.java@Suite@IncludeEngines("cucumber")@SelectClasspathResource("features")@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.dabbaghi.weather.cucumber")public class RunCucumberTest {}
8. Cucumber settings and the report output directory
Rather than hard-coding the glue path and plugins in RunCucumberTest, it’s common to put them in src/test/resources/junit-platform.properties, which Cucumber’s JUnit Platform engine reads automatically:
properties
# src/test/resources/junit-platform.propertiescucumber.glue=com.dabbaghi.weather.cucumbercucumber.plugin=pretty, \ html:target/cucumber-reports/cucumber-report.html, \ json:target/cucumber-reports/cucumber-report.json, \ junit:target/cucumber-reports/cucumber-report.xmlcucumber.publish.quiet=truecucumber.snippet-type=camelcase
A few notes on these settings:
cucumber.glueis where step definitions and hooks are discovered. It must match the package ofWeatherStepsandCucumberSpringConfiguration.cucumber.plugincontrols both the console output (pretty) and the report files. Each report format takes aformat:pathpair, and the path is the report’s output location — here everything lands undertarget/cucumber-reports/, alongside the rest of Maven’s build output, so it gets cleaned on everymvn cleanand is easy to publish from CI.cucumber.publish.quiet=truesilences the prompt to publish results to Cucumber’s hosted reports service.- If you’d rather configure things in the runner class instead of a properties file, the same settings work as
@ConfigurationParameterentries onRunCucumberTest.
9. A test that runs on any profile
The Cucumber scenario above is most useful when run against mock, since that’s the one profile with a deterministic, stubbed response. But it’s just as important to have a sanity check that has no opinion about which profile is active — useful as a smoke test in every environment, including a default mvn test with no system property set at all:
java
// WeatherApplicationSmokeTest.java@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@AutoConfigureRestTestClientclass WeatherApplicationSmokeTest { @Autowired private RestTestClient restTestClient; @Test void applicationStartsAndReportsHealthy() { restTestClient.get() .uri("/actuator/health") .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$.status").isEqualTo("UP"); }}
There’s deliberately no @ActiveProfiles annotation on this class. It doesn’t touch the WeatherClient or the downstream provider at all, so it passes identically whether you run it with default, dev, or mock active — it only verifies that the application context comes up and the embedded server responds.
10. Running everything
bash
# Cucumber scenario, backed by WireMock — the deterministic choice for CImvn test -Dspring.profiles.active=mock# Against the real dev environmentmvn test -Dspring.profiles.active=dev

Hinterlasse einen Kommentar