Skip to content

Commit

Permalink
[Spring] Document CucumberContextConfiguration semantics (#2887)
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 authored Sep 12, 2024
1 parent 36a0626 commit a50d1bb
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Fixed
- [Spring] Document `@CucumberContextConfiguration` semantics ([#2887](https://github.com/cucumber/cucumber-jvm/pull/2887) M.P. Korstanje)
- [Core] Enhanced stack trace to include step location for better debugging in case of datatable conversion errors ([#2908](https://github.com/cucumber/cucumber-jvm/pull/2908) Thomas Deblock)
- [Archetype] Set `cucumber.junit-platform.naming-strategy` to `long` when using Surefire.

Expand Down
80 changes: 79 additions & 1 deletion cucumber-spring/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ 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 Spring features that depend on a test execution
listeners, such as mock beans, will work on the annotated class - but not on
other step definition classes.

Step definition classes are instantiated and initialized by Spring. Features
that depend on beans initialisation, such as AspectJ, will work on step
definition classes - but not on the `@CucumberContextConfiguration` annotated
class.

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 +108,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 +129,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 @@ -21,7 +21,7 @@
* public class CucumberSpringConfiguration {
* }
* </pre>
*
* <p>
* Notes:
* <ul>
* <li>Only one glue class should be annotated with
Expand All @@ -30,6 +30,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 step
* definition classes.</li>
* <li></li>
* </ul>
*/
@Retention(RetentionPolicy.RUNTIME)
Expand Down

0 comments on commit a50d1bb

Please sign in to comment.