4

Working off of this post: Cucumber: no backend found when running from Spring Boot jar
I am attempting to get Cucumber to work full within a live Spring Boot app. I currently have a POC app using the aforementioned post's code to create a Cucumber Runtime and it runs a simple Feature/Steps class with a Bean referenced:
Service:

public void testFeature(){
        RuntimeOptions runtimeOptions = new RuntimeOptions(new ArrayList<String>(asList("--glue", "org.bdd.poc", "--no-dry-run", "--monochrome", "classpath:features")));
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ResourceLoader resourceLoader = new CustomMultiLoader(classLoader);
        ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
        Runtime runtime = new Runtime(resourceLoader, classFinder, classLoader, runtimeOptions);
        try {
            runtime.run();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

DemoSteps:

(no class annotation)
public class DemoSteps {

    @Autowired
    private MyBean bean;

    public DemoSteps(){
        System.err.println("STEP INIT");
    }

    @Given("a proof of concept")
    public void givenAPOC(){

    }

    @When("doing a demo")
    public void whenDemo(){

    }

    @Then("it should talk to a bean")
    public void thenItShouldTalkToABean(){
        this.bean.poke();
    }
}

Bean:

@Component
public class MyBean {
    public void poke(){
        System.err.println("I'VE BEEN POKED! THE PAIN!");
    }
}

The DemoSteps.java class has this as a class field with @Autowired in place, but while it is neither populating the bean reference or failing after DemoSteps construction. I see through debugging that there are spring bean factories being used to create the Steps instance, but nothing regarding autowiring is being touched. I am guessing in order to apply some connection to the main SpringContext lies in either:

  • An appropriate Backend implementation
  • An appropriate Glue implementation

I know that something similar was able to be accomplished using the Karate framework, but I have not found what is allowing that to make the connection.
Currently using Spring Boot 2.3.0 and Cucumber 2.4.0, unpacked per the "spring-boot-maven-plugin" config.

Toerktumlare
  • 12,548
  • 3
  • 35
  • 54
  • I think the route I am going to take is creating my own instance of `cucumber.runtime.java.JavaBackend` with a custom implementation of `cucumber.api.java.ObjectFactory`. Looking at the interface alone, it would appear that this is my bridge to the SpringContext. However, it almost appears that I would want to create my own scope to handle the step definitions or extend the existing `cucumber.runtime.java.spring.SpringFactory`. Some experimentation. – Joe Kennedy Aug 08 '20 at 02:12

1 Answers1

0

The solution I have was not quite the Backend, but a portion of it. The cucumber-spring dependency has an implementation of cucumber.api.java.ObjectFactory. This class acts as the access point for creating the Step classes, those which contain @Given/@When/etc. There is a 'test class' that does maintain this. However, I went the different route of creating a 'child' application context to the main one, AnnotationApplicationContext in particular to handle the @Autowired annotations. Anyway, here is the code for that:
Service:

Service Method:
    @Autowired(required = false)
    private List<LiveTestEventHandler> testEventHandlerList;
    @Autowired
    private ApplicationContext applicationContext;

    public void testFeature(){
        RuntimeOptions runtimeOptions = new RuntimeOptions(new ArrayList<String>(asList("--glue", "org.bddynamic.poc", "--no-dry-run", "--monochrome", "classpath:features")));
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ResourceLoader resourceLoader = new CustomMultiLoader(classLoader);
        ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
        ObjectFactory objectFactory = new LiveTestFactory(this.applicationContext);
        JavaBackend javaBackend = new JavaBackend(objectFactory,classFinder);
        Runtime runtime = new Runtime(resourceLoader, classLoader, asList(javaBackend), runtimeOptions);
        for(LiveTestEventHandler handler : this.testEventHandlerList){
            runtime.getEventBus().registerHandlerFor(handler.getSupportedClass(),handler);
        }
        try {
            runtime.run();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Custom Factory:

(Really cut down version of cucumber-spring:runtime.java.spring.SpringFactory.java)

public class LiveTestFactory implements ObjectFactory {
    private final DefaultListableBeanFactory beanFactory;
    private final GenericApplicationContext applicationContext;
    private final Collection<Class<?>> stepClasses = new HashSet();
    public LiveTestFactory(ApplicationContext applicationContext){
        AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext();
        childContext.setParent(applicationContext);
        this.applicationContext = childContext;
        this.beanFactory = (DefaultListableBeanFactory) this.applicationContext.getBeanFactory();
    }
    @Override
    public void start() {
        ((ConfigurableApplicationContext)this.applicationContext).registerShutdownHook();
        beanFactory.registerScope(LiveTestScope.NAME, new LiveTestScope());
        for(Class stepClass : this.stepClasses){
            this.registerStepClassBeanDefinition(this.beanFactory, stepClass);
        }
        this.applicationContext.refresh();
        this.applicationContext.start();
    }
    @Override
    public void stop() {
        LiveCodeContext.getInstance().stop();
        this.applicationContext.stop();
    }
    @Override
    public boolean addClass(Class<?> stepClass) {
        if (!this.stepClasses.contains(stepClass)) {
            this.stepClasses.add(stepClass);
        }
        return true;
    }
    @Override
    public <T> T getInstance(Class<T> type) {
        try {
            return this.applicationContext.getBean(type);
        } catch (BeansException var3) {
            throw new CucumberException(var3.getMessage(), var3);
        }
    }
    private void registerStepClassBeanDefinition(DefaultListableBeanFactory beanFactory, Class<?> stepClass) {
        BeanDefinitionRegistry registry = beanFactory;
        BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(stepClass).setScope(LiveTestScope.NAME).getBeanDefinition();
        registry.registerBeanDefinition(stepClass.getName(), beanDefinition);
    }
}