Skip to content

Commit

Permalink
[Spring] Document CucumberContextConfiguration semantics
Browse files Browse the repository at this point in the history
Cucumber uses Spring Test Context Manager framework. This framework was
written for JUnit and assumes that there is a "test instance".
Cucumber however uses multiple step definition classes and so it has
multiple test instances.

Originally `@CucumberContextConfiguration` was added to signal to Spring
which class should be used to configure the application context from.
But as people also expected mock beans and other features provided by
Springs test execution listeners to work (#2661) the annotated instance
was only instantiated but never initialized by Spring.

This changed the semantics somewhat as now features that depend on the
bean being initialized stopped working (#2886). Unfortunately, there is
little that can be done here. Spring expects that the instance provided
to the Test Context Manager to be an uninitialized bean. The solution
for this is to put the context configuration and step definitions in
different classes.

Cleaning up the examples to follow this pattern should avoid this
problem somewhat in the future. Though I won't go as far as recommending
people do this. Putting everything in one class looks quite nice. And
generally still works.

Closes: #2886
  • Loading branch information
mpkorstanje committed May 24, 2024
1 parent 2aae176 commit e39f05e
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 3 deletions.
76 changes: 75 additions & 1 deletion cucumber-spring/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public class CucumberSpringConfiguration {
Note: Cucumber Spring uses Spring's `TestContextManager` framework internally.
As a result, a single Cucumber scenario will mostly behave like a JUnit test.

The class annotated with `@CucumberContextConfiguration` is instantiated but not
initialized by Spring. Instead, this instance is processed by Springs test
execution listeners. So features that depend on a test execution listener such
as mock beans will work on the annotated class - but not on other step definition
classes. Features that depend on initializing beans - such as AspectJ - will not
work on the annotated class - but will work on other step definition classes.

For more information configuring Spring tests see:
- [Spring Framework Documentation - Testing](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/testing.html)
- [Spring Boot Features - Testing](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing)
Expand Down Expand Up @@ -97,7 +104,8 @@ Repeat as needed.
## Accessing the application context

Components from the application context can be accessed by autowiring.
Annotate a field in your step definition class with `@Autowired`.

Either annotate a field in your step definition class with `@Autowired`

```java
package com.example.app;
Expand All @@ -117,6 +125,72 @@ public class MyStepDefinitions {
}
```

Or declare a dependency through the constructor:

```java
package com.example.app;

import io.cucumber.java.en.Given;

public class MyStepDefinitions {

private final MyService myService;

public MyStepDefinitions(MyService myService){
this.myService = myService;
}

@Given("feed back is requested from my service")
public void feed_back_is_requested(){
myService.requestFeedBack();
}
}
```

## Using Mock Beans

To use mock beans, declare a mock bean in the context configuration.

```java
package com.example.app;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import io.cucumber.spring.CucumberContextConfiguration;

@CucumberContextConfiguration
@SpringBootTest(classes = TestConfig.class)
@MockBean(MyService.class)
public class CucumberSpringConfiguration {

}
```

Then in your step definitions, use the mock as you would normally.

```java
package com.example.app;

import org.springframework.beans.factory.annotation.Autowired;
import io.cucumber.java.en.Given;

import static org.mockito.Mockito.mockingDetails;
import static org.springframework.test.util.AssertionErrors.assertTrue;

public class MyStepDefinitions {

@Autowired
private MyService myService;

@Given("my service is a mock")
public void feed_back_is_requested(){
assertTrue(mockingDetails(myService).isMock());
}
}
```

## Sharing State

Cucumber Spring creates an application context and uses Spring's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
* {@code @BootstrapWith}. In case of SpringBoot, the configuration class can be
* annotated as follows:
* <p>
*
* <pre>
* &#64;CucumberContextConfiguration
* &#64;SpringBootTest(classes = TestConfig.class)
* public class CucumberSpringConfiguration {
* }
* </pre>
*
* <p>
* Notes:
* <ul>
* <li>Only one glue class should be annotated with
Expand All @@ -30,6 +29,15 @@
* <li>Cucumber Spring uses Spring's {@code TestContextManager} framework
* internally. As a result a single Cucumber scenario will mostly behave like a
* JUnit test.</li>
* <li>The class annotated with {@code CucumberContextConfiguration} is
* instantiated but not initialized by Spring. This instance is processed by
* Springs {@link org.springframework.test.context.TestExecutionListener
* TestExecutionListeners}. So features that depend on a test execution listener
* such as mock beans will work on the annotated class - but not on other step
* definition classes. Features that depend on initializing beans - such
* as AspectJ - will not work on the annotated class - but will work on other
* step definition classes.</li>
* <li></li>
* </ul>
*/
@Retention(RetentionPolicy.RUNTIME)
Expand Down

0 comments on commit e39f05e

Please sign in to comment.