Skip to content

Commit

Permalink
[Guice] Inject static fields prior to before all hooks (#2803)
Browse files Browse the repository at this point in the history
  • Loading branch information
mpkorstanje authored Nov 25, 2023
1 parent 5bf1679 commit 26329bb
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 39 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- [Guice] Inject static fields prior to before all hooks ([#2803](https://github.com/cucumber/cucumber-jvm/pull/2803) M.P. Korstanje)

## [7.14.0] - 2023-09-09
### Changed
Expand Down
14 changes: 12 additions & 2 deletions cucumber-guice/src/main/java/io/cucumber/guice/GuiceFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.util.Collection;
import java.util.HashSet;

import static io.cucumber.guice.InjectorSourceFactory.createDefaultScenarioModuleInjectorSource;
import static io.cucumber.guice.InjectorSourceFactory.instantiateUserSpecifiedInjectorSource;
import static io.cucumber.guice.InjectorSourceFactory.loadInjectorSourceFromProperties;
import static java.lang.String.format;

Expand All @@ -29,6 +31,10 @@ public final class GuiceFactory implements ObjectFactory {

public GuiceFactory() {
this.injectorSourceFromProperty = loadInjectorSourceFromProperties(CucumberProperties.create());
// Eager init to allow for static binding prior to before all hooks
if (this.injectorSourceFromProperty != null) {
injector = instantiateUserSpecifiedInjectorSource(this.injectorSourceFromProperty).getInjector();
}
}

@Override
Expand All @@ -40,6 +46,9 @@ public boolean addClass(final Class<?> stepClass) {
if (hasInjectorSource(stepClass)) {
checkOnlyOneClassHasInjectorSource(stepClass);
withInjectorSource = stepClass;
// Eager init to allow for static binding prior to before all
// hooks
injector = instantiateUserSpecifiedInjectorSource(withInjectorSource).getInjector();
}
}
stepClasses.add(stepClass);
Expand Down Expand Up @@ -69,9 +78,10 @@ void setInjector(Injector injector) {
}

public void start() {
// Last minute init. Neither properties not annotations provided an
// injector source.
if (injector == null) {
injector = new InjectorSourceFactory(withInjectorSource).create()
.getInjector();
injector = createDefaultScenarioModuleInjectorSource().getInjector();
}
scenarioScope = injector.getInstance(ScenarioScope.class);
scenarioScope.enterScope();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,16 @@
final class InjectorSourceFactory {
private static final Logger log = LoggerFactory.getLogger(GuiceFactory.class);
static final String GUICE_INJECTOR_SOURCE_KEY = "guice.injector-source";
private final Class<?> injectorSourceClass;

InjectorSourceFactory(Class<?> injectorSourceClass) {
this.injectorSourceClass = injectorSourceClass;
}

InjectorSource create() {
if (injectorSourceClass == null) {
return createDefaultScenarioModuleInjectorSource();
} else {
return instantiateUserSpecifiedInjectorSource(injectorSourceClass);
}
}

private InjectorSource createDefaultScenarioModuleInjectorSource() {
static InjectorSource createDefaultScenarioModuleInjectorSource() {
return () -> Guice.createInjector(Stage.PRODUCTION, CucumberModules.createScenarioModule());
}

private InjectorSource instantiateUserSpecifiedInjectorSource(Class<?> injectorSourceClass) {
static InjectorSource instantiateUserSpecifiedInjectorSource(Class<?> injectorSourceClass) {
try {
return (InjectorSource) injectorSourceClass.getConstructor().newInstance();
} catch (Exception e) {
String message = format("Instantiation of '%s' failed. Check the caused by exception and ensure your" +
String message = format("Instantiation of '%s' failed. Check the caused by exception and ensure your " +
"InjectorSource implementation is accessible and has a public zero args constructor.",
injectorSourceClass.getName());
throw new InjectorSourceInstantiationFailed(message, e);
Expand All @@ -57,7 +44,7 @@ static Class<?> loadInjectorSourceFromProperties(Map<String, String> properties)
try {
return Class.forName(injectorSourceClassName, true, Thread.currentThread().getContextClassLoader());
} catch (Exception e) {
String message = format("Instantiation of '%s' failed. Check the caused by exception and ensure your" +
String message = format("Instantiation of '%s' failed. Check the caused by exception and ensure your " +
"InjectorSource implementation is accessible and has a public zero args constructor.",
injectorSourceClassName);
throw new InjectorSourceInstantiationFailed(message, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Provides;
import com.google.inject.Scopes;
import com.google.inject.Stage;
import io.cucumber.core.backend.CucumberBackendException;
Expand All @@ -14,6 +15,7 @@
import io.cucumber.guice.integration.YourInjectorSource;
import io.cucumber.guice.matcher.ElementsAreAllEqualMatcher;
import io.cucumber.guice.matcher.ElementsAreAllUniqueMatcher;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
Expand All @@ -23,6 +25,7 @@
import java.util.List;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -242,6 +245,15 @@ void shouldThrowExceptionIfTwoDifferentInjectorSourcesAreFound() {
assertThat("Unexpected exception message", actualThrown.getMessage(), is(exceptionMessage));
}

@Test
void shouldInjectStaticBeforeStart() {
factory = new GuiceFactory();
WithStaticFieldClass.property = null;
factory.addClass(CucumberInjector.class);
assertThat(WithStaticFieldClass.property, equalTo("Hello world"));

}

static class UnscopedClass {

}
Expand All @@ -263,5 +275,30 @@ static class BoundScenarioScopedClass {
static class BoundSingletonClass {

}
static class WithStaticFieldClass {

@Inject
static String property;

}

public static class CucumberInjector implements InjectorSource {

@Override
public Injector getInjector() {
return Guice.createInjector(Stage.PRODUCTION, CucumberModules.createScenarioModule(), new AbstractModule() {
@Override
protected void configure() {
requestStaticInjection(WithStaticFieldClass.class);
}

@Singleton
@Provides
public String providesSomeString() {
return "Hello world";
}
});
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import java.util.Map;

import static io.cucumber.guice.InjectorSourceFactory.GUICE_INJECTOR_SOURCE_KEY;
import static org.hamcrest.CoreMatchers.instanceOf;
import static io.cucumber.guice.InjectorSourceFactory.instantiateUserSpecifiedInjectorSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.Is.isA;
Expand All @@ -18,12 +18,6 @@

class InjectorSourceFactoryTest {

@Test
void createsDefaultInjectorSourceWhenGuiceModulePropertyIsNotSet() {
InjectorSourceFactory injectorSourceFactory = new InjectorSourceFactory(null);
assertThat(injectorSourceFactory.create(), is(instanceOf(InjectorSource.class)));
}

@Test
void instantiatesInjectorSourceByFullyQualifiedName() {
Map<String, String> properties = new HashMap<>();
Expand All @@ -42,49 +36,43 @@ void failsToLoadNonExistantClass() {
() -> InjectorSourceFactory.loadInjectorSourceFromProperties(properties));
assertAll(
() -> assertThat("Unexpected exception message", actualThrown.getMessage(), is(equalTo(
"Instantiation of 'some.bogus.Class' failed. Check the caused by exception and ensure yourInjectorSource implementation is accessible and has a public zero args constructor."))),
"Instantiation of 'some.bogus.Class' failed. Check the caused by exception and ensure your InjectorSource implementation is accessible and has a public zero args constructor."))),
() -> assertThat("Unexpected exception cause class", actualThrown.getCause(),
isA(ClassNotFoundException.class)));
}

@Test
void failsToInstantiateClassNotImplementingInjectorSource() {
InjectorSourceFactory injectorSourceFactory = new InjectorSourceFactory(String.class);

Executable testMethod = injectorSourceFactory::create;
Executable testMethod = () -> instantiateUserSpecifiedInjectorSource(String.class);
InjectorSourceInstantiationFailed actualThrown = assertThrows(InjectorSourceInstantiationFailed.class,
testMethod);
assertAll(
() -> assertThat("Unexpected exception message", actualThrown.getMessage(), is(equalTo(
"Instantiation of 'java.lang.String' failed. Check the caused by exception and ensure yourInjectorSource implementation is accessible and has a public zero args constructor."))),
"Instantiation of 'java.lang.String' failed. Check the caused by exception and ensure your InjectorSource implementation is accessible and has a public zero args constructor."))),
() -> assertThat("Unexpected exception cause class", actualThrown.getCause(),
isA(ClassCastException.class)));
}

@Test
void failsToInstantiateClassWithPrivateConstructor() {
InjectorSourceFactory injectorSourceFactory = new InjectorSourceFactory(PrivateConstructor.class);

Executable testMethod = injectorSourceFactory::create;
Executable testMethod = () -> instantiateUserSpecifiedInjectorSource(PrivateConstructor.class);
InjectorSourceInstantiationFailed actualThrown = assertThrows(InjectorSourceInstantiationFailed.class,
testMethod);
assertAll(
() -> assertThat("Unexpected exception message", actualThrown.getMessage(), is(equalTo(
"Instantiation of 'io.cucumber.guice.InjectorSourceFactoryTest$PrivateConstructor' failed. Check the caused by exception and ensure yourInjectorSource implementation is accessible and has a public zero args constructor."))),
"Instantiation of 'io.cucumber.guice.InjectorSourceFactoryTest$PrivateConstructor' failed. Check the caused by exception and ensure your InjectorSource implementation is accessible and has a public zero args constructor."))),
() -> assertThat("Unexpected exception cause class", actualThrown.getCause(),
isA(NoSuchMethodException.class)));
}

@Test
void failsToInstantiateClassWithNoDefaultConstructor() {
InjectorSourceFactory injectorSourceFactory = new InjectorSourceFactory(NoDefaultConstructor.class);

Executable testMethod = injectorSourceFactory::create;
Executable testMethod = () -> instantiateUserSpecifiedInjectorSource(NoDefaultConstructor.class);
InjectorSourceInstantiationFailed actualThrown = assertThrows(InjectorSourceInstantiationFailed.class,
testMethod);
assertAll(
() -> assertThat("Unexpected exception message", actualThrown.getMessage(), is(equalTo(
"Instantiation of 'io.cucumber.guice.InjectorSourceFactoryTest$NoDefaultConstructor' failed. Check the caused by exception and ensure yourInjectorSource implementation is accessible and has a public zero args constructor."))),
"Instantiation of 'io.cucumber.guice.InjectorSourceFactoryTest$NoDefaultConstructor' failed. Check the caused by exception and ensure your InjectorSource implementation is accessible and has a public zero args constructor."))),
() -> assertThat("Unexpected exception cause class", actualThrown.getCause(),
isA(NoSuchMethodException.class)));
}
Expand Down

0 comments on commit 26329bb

Please sign in to comment.