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.java
public record WeatherForecast(String city, double temperature, String condition) {}

java

// WeatherClient.java
@Service
public 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
@SpringBootApplication
public 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.yml
spring:
application:
name: weather-service
weather:
api:
base-url: https://api.weather-provider.dabbaghi.com
connect-timeout: 2s
read-timeout: 3s
management:
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.yml
weather:
api:
base-url: https://dev.weather-provider.dabbaghi.com
connect-timeout: 3s
read-timeout: 5s
logging:
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.yml
weather:
api:
base-url: http://localhost:9561
connect-timeout: 1s
read-timeout: 2s
wiremock:
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.feature
Feature: 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.java
public 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.properties
cucumber.glue=com.dabbaghi.weather.cucumber
cucumber.plugin=pretty, \
html:target/cucumber-reports/cucumber-report.html, \
json:target/cucumber-reports/cucumber-report.json, \
junit:target/cucumber-reports/cucumber-report.xml
cucumber.publish.quiet=true
cucumber.snippet-type=camelcase

A few notes on these settings:

  • cucumber.glue is where step definitions and hooks are discovered. It must match the package of WeatherSteps and CucumberSpringConfiguration.
  • cucumber.plugin controls both the console output (pretty) and the report files. Each report format takes a format:path pair, and the path is the report’s output location — here everything lands under target/cucumber-reports/, alongside the rest of Maven’s build output, so it gets cleaned on every mvn clean and is easy to publish from CI.
  • cucumber.publish.quiet=true silences 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 @ConfigurationParameter entries on RunCucumberTest.

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)
@AutoConfigureRestTestClient
class 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 CI
mvn test -Dspring.profiles.active=mock
# Against the real dev environment
mvn test -Dspring.profiles.active=dev

Hinterlasse einen Kommentar

I’m Iman

Mein Name ist Iman Dabbaghi. Ich arbeite als Senior Software Engineer in der Schweiz. Außerdem interessiere ich mich sehr für gewaltfreie Kommunikation, Bachata-Tanz und Musik sowie fürs die Persönlichkeitsentwicklung.

Ich habe einen Masterabschluss in Informatik von der Universität Freiburg in Deutschland, bin Spring/Java Certified Professional (OCP), Certified Professional for Software Architecture (CPSA-F) und ein lebenslanger Lernender 🎓.

EN:

My name is Iman Dabbaghi. I work as a Senior Software Engineer in Switzerland. I am also very interessted in nonviolent communication, Bachata dance and music and also for personal development.

I hold a masters degree in computer science from the university of Freiburg in Germany, am a Spring / Java Certified Professional (OCP), Certified Software Architecture (CPSA-F) and Life Long Learner🎓

Let’s connect