Skip to content

[Spring] Require an active scenario before creating beans #1974

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] (In Git)

### Added
* [Spring] Add `@ScenarioScope` annotation ([#1974](https://github.com/cucumber/cucumber-jvm/issues/1974) M.P. Korstanje)
* Preferable to `@Scope(value = SCOPE_CUCUMBER_GLUE)`

### Changed

Expand All @@ -18,7 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
* [Plugin] Restored `Status.isOk(boolean isStrict)` to avoid breaking existing plugins
* [Core] Execute features files without pickles ([#1973](https://github.com/cucumber/cucumber-jvm/issues/1973) M.P. Korstanje)

* [Spring] Require an active scenario before creating beans ([#1974](https://github.com/cucumber/cucumber-jvm/issues/1974) M.P. Korstanje)
## [6.0.0-RC2] (2020-05-03)

### Added
Expand Down
11 changes: 5 additions & 6 deletions spring/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,18 @@ The `cucumber-glue` scope starts prior to a scenario and ends after a scenario.
All beans in this scope will be created before a scenario execution and
disposed at the end of it.

By using the `CucumberTestContext.SCOPE_CUCUMBER_GLUE` additional components
can be added to the glue scope. These components can be used to safely share
state between scenarios.
By using the `@ScenarioScope` annotation additional components can be added to
the glue scope. These components can be used to safely share state between
scenarios.

```java
package com.example.app;

import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Scope;
import static io.cucumber.spring.CucumberTestContext;
import io.cucumber.spring.ScenarioScope;

@Component
@Scope(CucumberTestContext.SCOPE_CUCUMBER_GLUE)
@ScenarioScope
public class TestUserInformation {

private User testUser;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

class GlueCodeScope implements Scope {
class CucumberScenarioScope implements Scope {

@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
GlueCodeContext context = GlueCodeContext.getInstance();
CucumberTestContext context = CucumberTestContext.getInstance();
Object obj = context.get(name);
if (obj == null) {
obj = objectFactory.getObject();
Expand All @@ -19,13 +19,13 @@ public Object get(String name, ObjectFactory<?> objectFactory) {

@Override
public Object remove(String name) {
GlueCodeContext context = GlueCodeContext.getInstance();
CucumberTestContext context = CucumberTestContext.getInstance();
return context.remove(name);
}

@Override
public void registerDestructionCallback(String name, Runnable callback) {
GlueCodeContext context = GlueCodeContext.getInstance();
CucumberTestContext context = CucumberTestContext.getInstance();
context.registerDestructionCallback(name, callback);
}

Expand All @@ -36,7 +36,7 @@ public Object resolveContextualObject(String key) {

@Override
public String getConversationId() {
GlueCodeContext context = GlueCodeContext.getInstance();
CucumberTestContext context = CucumberTestContext.getInstance();
return context.getId();
}

Expand Down
61 changes: 61 additions & 0 deletions spring/src/main/java/io/cucumber/spring/CucumberTestContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,73 @@

import org.apiguardian.api.API;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

@API(status = API.Status.STABLE)
public final class CucumberTestContext {

public static final String SCOPE_CUCUMBER_GLUE = "cucumber-glue";

private static final ThreadLocal<CucumberTestContext> localContext = ThreadLocal
.withInitial(CucumberTestContext::new);
private static final AtomicInteger sessionCounter = new AtomicInteger(0);

private final Map<String, Object> objects = new HashMap<>();
private final Map<String, Runnable> callbacks = new HashMap<>();

private Integer sessionId;

private CucumberTestContext() {
}

static CucumberTestContext getInstance() {
return localContext.get();
}

void start() {
sessionId = sessionCounter.incrementAndGet();
}

String getId() {
return "cucumber_test_context_" + sessionId;
}

void stop() {
for (Runnable callback : callbacks.values()) {
callback.run();
}
localContext.remove();
sessionId = null;
}

Object get(String name) {
requireActiveScenario();
return objects.get(name);
}

void put(String name, Object object) {
requireActiveScenario();
objects.put(name, object);
}

Object remove(String name) {
requireActiveScenario();
callbacks.remove(name);
return objects.remove(name);
}

void registerDestructionCallback(String name, Runnable callback) {
requireActiveScenario();
callbacks.put(name, callback);
}

void requireActiveScenario() {
if (sessionId == null) {
throw new IllegalStateException(
"Scenario scoped beans can only be created while Cucumber is executing a scenario");
}
}

}
58 changes: 0 additions & 58 deletions spring/src/main/java/io/cucumber/spring/GlueCodeContext.java

This file was deleted.

27 changes: 27 additions & 0 deletions spring/src/main/java/io/cucumber/spring/ScenarioScope.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.cucumber.spring;

import org.apiguardian.api.API;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks a bean as scoped to the execution of a cucumber scenario.
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(CucumberTestContext.SCOPE_CUCUMBER_GLUE)
@API(status = API.Status.STABLE)
public @interface ScenarioScope {
@AliasFor(
annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ public final void start() {
notifyContextManagerAboutTestClassStarted();
registerStepClassBeanDefinitions(applicationContext.getBeanFactory());
}
GlueCodeContext.getInstance().start();
CucumberTestContext.getInstance().start();
}

final void registerGlueCodeScope(ConfigurableApplicationContext context) {
while (context != null) {
context.getBeanFactory().registerScope(SCOPE_CUCUMBER_GLUE, new GlueCodeScope());
context.getBeanFactory().registerScope(SCOPE_CUCUMBER_GLUE, new CucumberScenarioScope());
context = (ConfigurableApplicationContext) context.getParent();
}
}
Expand Down Expand Up @@ -91,7 +91,7 @@ private void registerStepClassBeanDefinition(BeanDefinitionRegistry registry, Cl
}

public final void stop() {
GlueCodeContext.getInstance().stop();
CucumberTestContext.getInstance().stop();
try {
delegate.afterTestClass();
} catch (Exception e) {
Expand Down
88 changes: 88 additions & 0 deletions spring/src/test/java/io/cucumber/spring/Issue1970.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.cucumber.spring;

import io.cucumber.core.backend.ObjectFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;

import java.util.concurrent.atomic.AtomicInteger;

import static io.cucumber.spring.CucumberTestContext.SCOPE_CUCUMBER_GLUE;
import static org.junit.Assert.assertNotEquals;

class Issue1970 {

@Test
public void issue1970() {
ObjectFactory factory = new SpringFactory();
factory.addClass(GlueClass.class); // Add glue with Spring configuration
factory.start();
GlueClass instance = factory.getInstance(GlueClass.class);
String response = instance.service.get();
factory.stop();
factory.start();
GlueClass instance2 = factory.getInstance(GlueClass.class);
String response2 = instance2.service.get();
factory.stop();

assertNotEquals(response, response2);
}

@CucumberContextConfiguration
@ContextConfiguration(classes = TestApplicationConfiguration.class)
public static class GlueClass {

@Autowired
ExampleService service;

}

@Configuration
public static class TestApplicationConfiguration {

@Bean
public BeanFactoryPostProcessor beanFactoryPostProcessor() {
return factory -> factory.registerScope(SCOPE_CUCUMBER_GLUE, new CucumberScenarioScope());
}

@Bean
public ExampleService service(ScenarioScopedApi api) {
return new ExampleService(api);
}

@Bean
@ScenarioScope
public ScenarioScopedApi api() {
return new ScenarioScopedApi();
}

}

public static class ExampleService {

final ScenarioScopedApi api;

public ExampleService(ScenarioScopedApi api) {
this.api = api;
}

String get() {
return "Api response: " + api.get();
}
}

public static class ScenarioScopedApi {

private static final AtomicInteger globalCounter = new AtomicInteger(0);
private final int instanceId = globalCounter.getAndIncrement();

public String get() {
return "instance " + instanceId;
}

}

}
Loading