Cucumber with Guice - multiple guice injector

2019-06-28 03:05发布

I'm using Cucumber with Guice as DI. I've encountered following problem: I've got one step i.e.

class MyStep() {

    @Inject
    private MyService myService;

    @Given("Some acction happen")
    public void sthHappen() {
        myService.doSth();
    }
}

And I've got this class to run it as JUnit test

@RunWith(Cucumber.class)
@CucumberOptions(...)
public class MyTest {

}

There is a

class MyModule extends AbstractModule {
    @Override
    protected void configure() {
         bind(MyService.class).to(MyFirstService.class);     
    }
}

which is used by my MyInjectorSource I define cucumber.properties where I define guice.injector-source=MyInjectorSource; There is also a feature file with scenario. Everything is working for now.

And no i would like to run MyStep step with other MyService implementation (of course I don't wont to duplicate code of MyStep) I define a new feature file with new scenarios, and new Test class

@RunWith(Cucumber.class)
@CucumberOptions(...)
public class MyOtherTest {

}

And now I've tried to create another InjectorSource but I was not able to configure it.

1条回答
劫难
2楼-- · 2019-06-28 03:32

Solution which I've found is using custom Junit4 runner inheriting from original Cucumber runner and changing its createRuntime method.

Latest cucumber-guice 1.2.5 uses few stages to create injector and unfortunately it uses global variable cucumber.runtime.Env.INSTANCE. This variable is populated from cucumber.properties and System.getProperties.

Flow is:

  • Cucumber runner scans available backends (in my setup it is cucumber.runtime.java.JavaBackend)
  • One of JavaBackend constructor loads available ObjectFactory (in my setup it is cucumber.runtime.java.guice.impl.GuiceFactory)
  • GuiceFactory via InjectorSourceFactory checks Env.INSTANCE, it will create custom InjectorSource or default injector

Ideally cucumber should pass its 'RuntimeOptions` created at start to backend and InjectorSource but unfortunately it doesn't and uses global variable. It is not easy create patch like this one so my solution simplifies this approach and directly create InjectorSource in custom runner by reading new annotation.

public class GuiceCucumberRunner extends Cucumber {

    public GuiceCucumberRunner(Class<?> clazz) throws InitializationError, IOException {
        super(clazz);
    }

    @Override
    protected Runtime createRuntime(ResourceLoader resourceLoader, ClassLoader classLoader, RuntimeOptions runtimeOptions) throws InitializationError, IOException {
        Runtime result = new Runtime(resourceLoader, classLoader, Arrays.asList(createGuiceBackend()), runtimeOptions);
        return result;
    }

    private JavaBackend createGuiceBackend() {
        GuiceCucumberOptions guiceCucumberOptions = getGuiceCucumberOptions(); 
        InjectorSource injectorSource = createInjectorSource(guiceCucumberOptions.injectorSource());
        ObjectFactory objectFactory = new GuiceFactory(injectorSource.getInjector());
        JavaBackend result = new JavaBackend(objectFactory);
        return result;
    }

    private GuiceCucumberOptions getGuiceCucumberOptions() {
        GuiceCucumberOptions guiceCucumberOptions = getTestClass().getJavaClass().getAnnotation(GuiceCucumberOptions.class);
        if (guiceCucumberOptions == null) {
            String message = format("Suite class ''{0}'' is missing annotation GuiceCucumberOptions", getTestClass().getJavaClass());
            throw new CucumberException(message);
        }
        return guiceCucumberOptions;
    }

    private InjectorSource createInjectorSource(Class<? extends InjectorSource> clazz) {
        try {
            return clazz.newInstance();
        } catch (Exception e) {
            String message = format("Instantiation of ''{0}'' failed. InjectorSource must have has a public zero args constructor.", clazz);
            throw new InjectorSourceInstantiationFailed(message, e);
        }
    }

    static class GuiceFactory implements ObjectFactory {

        private final Injector injector;

        GuiceFactory(Injector injector) {
            this.injector = injector;
        }

        @Override
        public boolean addClass(Class<?> clazz) {
            return true;
        }

        @Override
        public void start() {
            injector.getInstance(ScenarioScope.class).enterScope();
        }

        @Override
        public void stop() {
            injector.getInstance(ScenarioScope.class).exitScope();
        }

        @Override
        public <T> T getInstance(Class<T> clazz) {
            return injector.getInstance(clazz);
        }
    }
}

 @Retention(RetentionPolicy.RUNTIME)
 @Target({ ElementType.TYPE })
 public @interface GuiceCucumberOptions {

     Class<? extends InjectorSource> injectorSource();

 }

@RunWith(GuiceCucumberRunner.class)
@GuiceCucumberOptions(injectorSource = MyInjector.class)
@CucumberOptions(
        ...
)
public class Suite {

}

I needed to copy GuiceFactory because it doesn't exposes normal constructor (!)

查看更多
登录 后发表回答