Skip to content

Instantly share code, notes, and snippets.

@slamdev
Created February 12, 2018 15:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slamdev/c56e97f80cd4d82c1b02e013a589adea to your computer and use it in GitHub Desktop.
Save slamdev/c56e97f80cd4d82c1b02e013a589adea to your computer and use it in GitHub Desktop.
cukes-with-spring-boot
cucumber.api.java.ObjectFactory=SpringFactory
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootContextLoader;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration(classes = Application.class, loader = SpringBootContextLoader.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
public @interface CucumberStepsDefinition {
}
import org.junit.runner.RunWith;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
@RunWith(Cucumber.class)
@CucumberOptions(
features = {"classpath:cucumber/"},
glue = {"lv.ctco.cukes", "cucumber"},
strict = true
)
public class RunnerIT {
}
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import cucumber.api.java.en.Then;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@CucumberStepsDefinition
public class SampleSteps {
@Autowired
private TestRestTemplate restTemplate;
@Then("verify")
public void verify() {
final ResponseEntity<String> response = restTemplate.getForEntity(
"/health",
String.class
);
log.info("{}", response);
assertThat(response, notNullValue());
}
}
import static com.google.common.base.Preconditions.checkState;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.reflections.Reflections;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.TestContextManager;
import com.google.common.collect.Sets;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Stage;
import cucumber.api.guice.CucumberModules;
import cucumber.api.java.ObjectFactory;
import cucumber.runtime.CucumberException;
import cucumber.runtime.java.guice.ScenarioScope;
import lv.ctco.cukes.core.CukesRuntimeException;
import lv.ctco.cukes.core.extension.CukesInjectableModule;
import lv.ctco.cukes.core.internal.di.CukesGuiceModule;
import lv.ctco.cukes.core.internal.di.SingletonObjectFactory;
/**
* Spring based implementation of ObjectFactory.
* <p/>
* <p>
* <ul>
* <li>It uses TestContextManager to manage the spring context.
* Configuration via: @ContextConfiguration or @ContextHierarcy
* At least on step definition class needs to have a @ContextConfiguration or
*
* @ContextHierarchy annotation. If more that one step definition class has such
* an annotation, the annotations must be equal on the different step definition
* classes. If no step definition class with @ContextConfiguration or
* @ContextHierarcy is found, it will try to load cucumber.xml from the classpath.
* </li>
* <li>The step definitions class with @ContextConfiguration or @ContextHierarchy
* annotation, may also have a @WebAppConfiguration or @DirtiesContext annotation.
* </li>
* <li>The step definitions added to the TestContextManagers context and
* is reloaded for each scenario.</li>
* </ul>
* </p>
* <p/>
* <p>
* Application beans are accessible from the step definitions using autowiring
* (with annotations).
* </p>
*/
public class SpringFactory implements ObjectFactory {
private ConfigurableListableBeanFactory beanFactory;
private CucumberTestContextManager testContextManager;
private final Collection<Class<?>> stepClasses = new HashSet<Class<?>>();
private Class<?> stepClassWithSpringContext = null;
private static final Set<Module> MODULES = Sets.newConcurrentHashSet();
static {
MODULES.add(CucumberModules.SCENARIO);
MODULES.add(new CukesGuiceModule());
}
private static Injector injector = null;
public SpringFactory() {
}
@Override
public boolean addClass(final Class<?> stepClass) {
if (!stepClasses.contains(stepClass)) {
if (dependsOnSpringContext(stepClass)) {
if (stepClassWithSpringContext == null) {
stepClassWithSpringContext = stepClass;
} else {
checkAnnotationsEqual(stepClassWithSpringContext, stepClass);
}
}
stepClasses.add(stepClass);
}
return true;
}
private void checkAnnotationsEqual(Class<?> stepClassWithSpringContext, Class<?> stepClass) {
Annotation[] annotations1 = stepClassWithSpringContext.getAnnotations();
Annotation[] annotations2 = stepClass.getAnnotations();
if (annotations1.length != annotations2.length) {
throw new CucumberException("Annotations differs on glue classes found: " +
stepClassWithSpringContext.getName() + ", " +
stepClass.getName());
}
for (Annotation annotation : annotations1) {
if (!isAnnotationInArray(annotation, annotations2)) {
throw new CucumberException("Annotations differs on glue classes found: " +
stepClassWithSpringContext.getName() + ", " +
stepClass.getName());
}
}
}
private boolean isAnnotationInArray(Annotation annotation, Annotation[] annotations) {
for (Annotation annotationFromArray : annotations) {
if (annotation.equals(annotationFromArray)) {
return true;
}
}
return false;
}
@Override
public void start() {
lazyInitInjector();
injector.getInstance(ScenarioScope.class).enterScope();
if (stepClassWithSpringContext != null) {
testContextManager = new CucumberTestContextManager(stepClassWithSpringContext);
} else {
if (beanFactory == null) {
beanFactory = createFallbackContext();
}
}
notifyContextManagerAboutTestClassStarted();
if (beanFactory == null || isNewContextCreated()) {
beanFactory = testContextManager.getBeanFactory();
for (Class<?> stepClass : stepClasses) {
registerStepClassBeanDefinition(beanFactory, stepClass);
}
}
GlueCodeContext.INSTANCE.start();
}
@SuppressWarnings("resource")
private ConfigurableListableBeanFactory createFallbackContext() {
ConfigurableApplicationContext applicationContext;
if (getClass().getClassLoader().getResource("cucumber.xml") != null) {
applicationContext = new ClassPathXmlApplicationContext("cucumber.xml");
} else {
applicationContext = new GenericApplicationContext();
}
applicationContext.registerShutdownHook();
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
beanFactory.registerScope(GlueCodeScope.NAME, new GlueCodeScope());
for (Class<?> stepClass : stepClasses) {
registerStepClassBeanDefinition(beanFactory, stepClass);
}
return beanFactory;
}
private void notifyContextManagerAboutTestClassStarted() {
if (testContextManager != null) {
try {
testContextManager.beforeTestClass();
} catch (Exception e) {
throw new CucumberException(e.getMessage(), e);
}
}
}
private boolean isNewContextCreated() {
if (testContextManager == null) {
return false;
}
return !beanFactory.equals(testContextManager.getBeanFactory());
}
private void registerStepClassBeanDefinition(ConfigurableListableBeanFactory beanFactory, Class<?> stepClass) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
BeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(stepClass)
.setScope(GlueCodeScope.NAME)
.getBeanDefinition();
registry.registerBeanDefinition(stepClass.getName(), beanDefinition);
}
@Override
public void stop() {
notifyContextManagerAboutTestClassFinished();
GlueCodeContext.INSTANCE.stop();
lazyInitInjector();
injector.getInstance(ScenarioScope.class).exitScope();
}
private void notifyContextManagerAboutTestClassFinished() {
if (testContextManager != null) {
try {
testContextManager.afterTestClass();
} catch (Exception e) {
throw new CucumberException(e.getMessage(), e);
}
}
}
@Override
public <T> T getInstance(final Class<T> type) {
if (type.getPackage().getName().startsWith(getClass().getPackage().getName())) {
try {
return beanFactory.getBean(type);
} catch (BeansException e) {
throw new CucumberException(e.getMessage(), e);
}
}
lazyInitInjector();
return injector.getInstance(type);
}
private boolean dependsOnSpringContext(Class<?> type) {
boolean hasStandardAnnotations = annotatedWithSupportedSpringRootTestAnnotations(type);
if (hasStandardAnnotations) {
return true;
}
final Annotation[] annotations = type.getDeclaredAnnotations();
return (annotations.length == 1) &&
annotatedWithSupportedSpringRootTestAnnotations(annotations[0].annotationType());
}
private boolean annotatedWithSupportedSpringRootTestAnnotations(Class<?> type) {
return type.isAnnotationPresent(ContextConfiguration.class)
|| type.isAnnotationPresent(ContextHierarchy.class);
}
public void addModule(Module module) {
checkState(injector == null, "Cannot add modules after the factory has been used!");
MODULES.add(module);
}
private void lazyInitInjector() {
if (injector == null) {
addExternalModules();
injector = Guice.createInjector(Stage.PRODUCTION, MODULES);
}
}
private void addExternalModules() {
Reflections reflections = new Reflections("lv.ctco.cukes");
for (Class targetClass : reflections.getTypesAnnotatedWith(CukesInjectableModule.class)) {
try {
Constructor<Module> constructor = targetClass.getConstructor();
Module module = constructor.newInstance();
addModule(module);
} catch (Exception e) {
throw new CukesRuntimeException("Unable to add External Module to Guice");
}
}
}
public static SingletonObjectFactory instance() {
return SpringFactory.InstanceHolder.INSTANCE;
}
private static class InstanceHolder {
static final SingletonObjectFactory INSTANCE = new SingletonObjectFactory();
}
}
class CucumberTestContextManager extends TestContextManager {
public CucumberTestContextManager(Class<?> testClass) {
super(testClass);
registerGlueCodeScope(getContext());
}
public ConfigurableListableBeanFactory getBeanFactory() {
return getContext().getBeanFactory();
}
private ConfigurableApplicationContext getContext() {
return (ConfigurableApplicationContext) getTestContext().getApplicationContext();
}
private void registerGlueCodeScope(ConfigurableApplicationContext context) {
do {
context.getBeanFactory().registerScope(GlueCodeScope.NAME, new GlueCodeScope());
context = (ConfigurableApplicationContext) context.getParent();
} while (context != null);
}
}
class GlueCodeContext {
public static final GlueCodeContext INSTANCE = new GlueCodeContext();
private final Map<String, Object> objects = new HashMap<String, Object>();
private final Map<String, Runnable> callbacks = new HashMap<String, Runnable>();
private int counter;
private GlueCodeContext() {
}
public void start() {
cleanUp();
counter++;
}
public String getId() {
return "cucumber_glue_" + counter;
}
public void stop() {
for (Runnable callback : callbacks.values()) {
callback.run();
}
cleanUp();
}
public Object get(String name) {
return objects.get(name);
}
public void put(String name, Object object) {
objects.put(name, object);
}
public Object remove(String name) {
callbacks.remove(name);
return objects.remove(name);
}
private void cleanUp() {
objects.clear();
callbacks.clear();
}
public void registerDestructionCallback(String name, Runnable callback) {
callbacks.put(name, callback);
}
}
class GlueCodeScope implements Scope {
public static final String NAME = "cucumber-glue";
private final GlueCodeContext context = GlueCodeContext.INSTANCE;
@Override
public Object get(String name, org.springframework.beans.factory.ObjectFactory<?> objectFactory) {
Object obj = context.get(name);
if (obj == null) {
obj = objectFactory.getObject();
context.put(name, obj);
}
return obj;
}
@Override
public Object remove(String name) {
return context.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
context.registerDestructionCallback(name, callback);
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return context.getId();
}
}
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.SimpleTransactionStatus;
import cucumber.api.java.After;
import cucumber.api.java.Before;
/**
* <p>
* This class defines before and after hooks which provide automatic spring rollback capabilities.
* These hooks will apply to any element(s) within a <code>.feature</code> file tagged with <code>@txn</code>.
* </p>
* <p>
* Clients wishing to leverage these hooks should include this class' package in the <code>glue</code> code.
* </p>
* <p>
* The BEFORE and AFTER hooks (both with hook order 100) rely on being able to obtain a <code>PlatformTransactionManager</code> by type, or
* by an optionally specified bean name, from the runtime <code>BeanFactory</code>.
* </p>
* <p>
* NOTE: This class is NOT threadsafe! It relies on the fact that cucumber-jvm will instantiate an instance of any
* applicable hookdef class per scenario run.
* </p>
*/
public class SpringTransactionHooks implements BeanFactoryAware {
private BeanFactory beanFactory;
private String txnManagerBeanName;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* @return the (optional) bean name for the transaction manager to be obtained - if null, attempt will be made to find a transaction manager by bean type
*/
public String getTxnManagerBeanName() {
return txnManagerBeanName;
}
/**
* Setter to allow (optional) bean name to be specified for transaction manager bean - if null, attempt will be made to find a transaction manager by bean type
*
* @param txnManagerBeanName bean name of transaction manager bean
*/
public void setTxnManagerBeanName(String txnManagerBeanName) {
this.txnManagerBeanName = txnManagerBeanName;
}
private TransactionStatus transactionStatus;
@Before(value = {"@txn"}, order = 100)
public void startTransaction() {
transactionStatus = obtainPlatformTransactionManager().getTransaction(new DefaultTransactionDefinition());
}
@After(value = {"@txn"}, order = 100)
public void rollBackTransaction() {
obtainPlatformTransactionManager().rollback(transactionStatus);
}
public PlatformTransactionManager obtainPlatformTransactionManager() {
if (getTxnManagerBeanName() == null) {
return beanFactory.getBean(PlatformTransactionManager.class);
} else {
return beanFactory.getBean(txnManagerBeanName, PlatformTransactionManager.class);
}
}
public TransactionStatus getTransactionStatus() {
return transactionStatus;
}
public void setTransactionStatus(SimpleTransactionStatus transactionStatus) {
this.transactionStatus = transactionStatus;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment