From a50d1bb03ed9ff9501c033e0469075aa7e48d1ff Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 12 Sep 2024 23:00:30 +0200 Subject: [PATCH] [Spring] Document CucumberContextConfiguration semantics (#2887) 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 --- CHANGELOG.md | 1 + cucumber-spring/README.md | 80 ++++++++++++++++++- .../spring/CucumberContextConfiguration.java | 11 ++- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b11c5edb9..c5ee934de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/cucumber-spring/README.md b/cucumber-spring/README.md index 1e661cdd04..65e423e65e 100644 --- a/cucumber-spring/README.md +++ b/cucumber-spring/README.md @@ -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) @@ -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; @@ -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 diff --git a/cucumber-spring/src/main/java/io/cucumber/spring/CucumberContextConfiguration.java b/cucumber-spring/src/main/java/io/cucumber/spring/CucumberContextConfiguration.java index 96ed6692a6..e54d5388d2 100644 --- a/cucumber-spring/src/main/java/io/cucumber/spring/CucumberContextConfiguration.java +++ b/cucumber-spring/src/main/java/io/cucumber/spring/CucumberContextConfiguration.java @@ -21,7 +21,7 @@ * public class CucumberSpringConfiguration { * } * - * + *

* Notes: *

*/ @Retention(RetentionPolicy.RUNTIME)