在单元测试期间创建自定义 @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);
}

回答我的问题

  1. 创建 TestScoped 注释:
import com.google.inject.ScopeAnnotation;

@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@ScopeAnnotation
public @interface TestScoped
{}

  1. 创建一个 TestScope class 来管理 TestScoped 中实例的生命周期:
package com.see.os.configuration.context.unittests;

public class TestScope extends SimpleScope
{}

在这里,SimpleScopeGuice 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.&lt;KeyClass&gt;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;
  }
}
  1. @TestScoped
  2. 注释您的 class
@TestScoped
class ScopedObj
{}
  1. 在您的注入配置中,将 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);
        }
    });

  1. 在单元测试中,您不能在测试开始和结束时“进入”和“退出”作用域。
    @BeforeEach
    void onBeginTest() {
        m_injector.getInstance(TestScope.class).enter();
    }

    @AfterEach
    void onFinishTest() {
        m_injector.getInstance(TestScope.class).exit();
    }
  1. 您的测试现在可以期望 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