在单元测试期间创建自定义 @TestScoped Guice 范围
Creating a custom @TestScoped Guice scope for the duration of a unit test
我想限定对象的范围,以便它们在单元测试期间作为单例存在。该模式将遵循已经在 Google Guice 中实现的 @RequestScoped 和 @SessionScoped,通过围绕 junit 测试的 @Before 和 @After 范围:
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
}
}
Google Guice 可以吗?
如果你使用 GuiceBerry(基本上是用于测试的 Guice/jUnit),你可以使用 @TestScoped
import com.google.guiceberry.TestScoped;
@Provides
@TestScoped
FlyingSquirrel provideFlyingSquirrel() {
return new WhaleFlyingSquirrelImpl(42);
}
回答我的问题
- 创建
TestScoped
注释:
import com.google.inject.ScopeAnnotation;
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@ScopeAnnotation
public @interface TestScoped
{}
- 创建一个
TestScope
class 来管理 TestScoped
中实例的生命周期:
package com.see.os.configuration.context.unittests;
public class TestScope extends SimpleScope
{}
在这里,SimpleScope
是 Guice documentation 中的那个。包括完整性:
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
注释您的 class
@TestScoped
class ScopedObj
{}
- 在您的注入配置中,将
TestScoped
注释关联到 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
个对象在测试期间作为单例持续存在并在每次测试之间重置:
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;
}
我在这里提供了完整的样本:https://gist.github.com/gaspardpetit/61e95523a6a7672dd65c97a913cce33f
我想限定对象的范围,以便它们在单元测试期间作为单例存在。该模式将遵循已经在 Google Guice 中实现的 @RequestScoped 和 @SessionScoped,通过围绕 junit 测试的 @Before 和 @After 范围:
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
}
}
Google Guice 可以吗?
如果你使用 GuiceBerry(基本上是用于测试的 Guice/jUnit),你可以使用 @TestScoped
import com.google.guiceberry.TestScoped;
@Provides
@TestScoped
FlyingSquirrel provideFlyingSquirrel() {
return new WhaleFlyingSquirrelImpl(42);
}
回答我的问题
- 创建
TestScoped
注释:
import com.google.inject.ScopeAnnotation;
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@ScopeAnnotation
public @interface TestScoped
{}
- 创建一个
TestScope
class 来管理TestScoped
中实例的生命周期:
package com.see.os.configuration.context.unittests;
public class TestScope extends SimpleScope
{}
在这里,SimpleScope
是 Guice documentation 中的那个。包括完整性:
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
注释您的 class
@TestScoped
class ScopedObj
{}
- 在您的注入配置中,将
TestScoped
注释关联到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
个对象在测试期间作为单例持续存在并在每次测试之间重置:
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;
}
我在这里提供了完整的样本:https://gist.github.com/gaspardpetit/61e95523a6a7672dd65c97a913cce33f