I would like to scope objects so that they exist as singletons for the duration of a unit test. The pattern would follow the @RequestScoped and @SessionScoped already implemented in Google Guice, by would scope around the @Before and the @After of a junit test:
public class MyUnitTest {
@TestScoped static class MyTestScopedClass { }
@Before public void enterTestScope() {
// something here creates the TestScope
}
@After public void exitTestScope() {
// destroy the TestScope
}
@Test public void MyTest() {
// MyTest instantiates object using injection, some are @Singletons
// and will remain for other tests, but those that are @TestScoped
// will be created once for this test and will be automatically
// destroyed at the end of this test
}
}
Is this possible with Google Guice ?
Answering my question
TestScoped
annotation:import com.google.inject.ScopeAnnotation;
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@ScopeAnnotation
public @interface TestScoped
{}
TestScope
class that will manage the lifecycle of the instances in TestScoped
:package com.see.os.configuration.context.unittests;
public class TestScope extends SimpleScope
{}
Here, the SimpleScope
is the one from the Guice documentation. Including for completeness:
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.Maps;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.Scope;
import java.util.Map;
/**
* Scopes a single execution of a block of code. Apply this scope with a
* try/finally block: <pre><code>
*
* scope.enter();
* try {
* // explicitly seed some seed objects...
* scope.seed(Key.get(SomeObject.class), someObject);
* // create and access scoped objects
* } finally {
* scope.exit();
* }
* </code></pre>
*
* The scope can be initialized with one or more seed values by calling
* <code>seed(key, value)</code> before the injector will be called upon to
* provide for this key. A typical use is for a servlet filter to enter/exit the
* scope, representing a Request Scope, and seed HttpServletRequest and
* HttpServletResponse. For each key inserted with seed(), you must include a
* corresponding binding:
* <pre><code>
* bind(key)
* .toProvider(SimpleScope.<KeyClass>seededKeyProvider())
* .in(ScopeAnnotation.class);
* </code></pre>
*
* @author Jesse Wilson
* @author Fedor Karpelevitch
*/
public class SimpleScope implements Scope {
private static final Provider<Object> SEEDED_KEY_PROVIDER =
new Provider<Object>() {
public Object get() {
throw new IllegalStateException("If you got here then it means that" +
" your code asked for scoped object which should have been" +
" explicitly seeded in this scope by calling" +
" SimpleScope.seed(), but was not.");
}
};
private final ThreadLocal<Map<Key<?>, Object>> values
= new ThreadLocal<Map<Key<?>, Object>>();
public void enter() {
checkState(values.get() == null, "A scoping block is already in progress");
values.set(Maps.<Key<?>, Object>newHashMap());
}
public void exit() {
checkState(values.get() != null, "No scoping block in progress");
values.remove();
}
public <T> void seed(Key<T> key, T value) {
Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);
checkState(!scopedObjects.containsKey(key), "A value for the key %s was " +
"already seeded in this scope. Old value: %s New value: %s", key,
scopedObjects.get(key), value);
scopedObjects.put(key, value);
}
public <T> void seed(Class<T> clazz, T value) {
seed(Key.get(clazz), value);
}
public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
return new Provider<T>() {
public T get() {
Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);
@SuppressWarnings("unchecked")
T current = (T) scopedObjects.get(key);
if (current == null && !scopedObjects.containsKey(key)) {
current = unscoped.get();
// don't remember proxies; these exist only to serve circular dependencies
if (Scopes.isCircularProxy(current)) {
return current;
}
scopedObjects.put(key, current);
}
return current;
}
};
}
private <T> Map<Key<?>, Object> getScopedObjectMap(Key<T> key) {
Map<Key<?>, Object> scopedObjects = values.get();
if (scopedObjects == null) {
throw new OutOfScopeException("Cannot access " + key
+ " outside of a scoping block");
}
return scopedObjects;
}
/**
* Returns a provider that always throws exception complaining that the object
* in question must be seeded before it can be injected.
*
* @return typed provider
*/
@SuppressWarnings({"unchecked"})
public static <T> Provider<T> seededKeyProvider() {
return (Provider<T>) SEEDED_KEY_PROVIDER;
}
}
@TestScoped
@TestScoped
class ScopedObj
{}
TestScoped
annotation to an instance of TestScope
:class TestScopeTest
{
private final Injector m_injector = Guice.createInjector(new AbstractModule() {
@Override protected void configure() {
TestScope testScope = new TestScope();
bindScope(TestScoped.class, testScope);
bind(TestScope.class).toInstance(testScope);
}
});
@BeforeEach
void onBeginTest() {
m_injector.getInstance(TestScope.class).enter();
}
@AfterEach
void onFinishTest() {
m_injector.getInstance(TestScope.class).exit();
}
TestScoped
objects to persists as singletons for the duration of the test and be reset between each tests: static ScopedObj s_previousScopedObj;
@RepeatedTest(2)
void givenTest_whenGetTestScoped_ensureIsScopedToTest() {
// when
ScopedObj obj = m_injector.getInstance(ScopedObj.class);
ScopedObj objAgain = m_injector.getInstance(ScopedObj.class);
// ensure
Assertions.assertSame(obj, objAgain);
Assertions.assertNotSame(s_previousScopedObj, obj);
s_previousScopedObj = objAgain;
}
I provided a full sample here: https://gist.github.com/gaspardpetit/61e95523a6a7672dd65c97a913cce33f