Created
February 12, 2018 15:27
-
-
Save slamdev/c56e97f80cd4d82c1b02e013a589adea to your computer and use it in GitHub Desktop.
cukes-with-spring-boot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
cucumber.api.java.ObjectFactory=SpringFactory |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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