在JUnit测试中使用Guice注入器

29

在使用 Guice 时,每个 JUnit 测试类中获取一个新的注入器是一个好的实践吗?因为每个测试类都应该是独立的。

7个回答

45

如果有人偶然遇到了这个问题,并想要了解如何从单元测试中使用Guice注释,则可以从类似下面的基类扩展你的测试,并调用injector.injectMembers(this);

public class TestBase {
    protected Injector injector = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
            bind(HelloService.class);
        }
    });

    @Before
    public void setup () {
        injector.injectMembers(this);
    }
}

然后您的测试可以像这样获取一个注入的HelloService

public class HelloServiceTest extends TestBase {
    @Inject
    HelloService service;

    @Test
    public void testService() throws Exception {
       //Do testing here
    }
}

1
你应该注意将 injectMembers 应用于你想要测试和需要注入的类,而不仅仅是测试类中的 this - Mahdi
应该是HelloServiceTest,而不是HelloServletTest,以及HelloService service;而不是HelloServlet servlet;。我假设是这样的,并编辑了你的答案。 - Ray
TestBase 应该是 抽象的(abstract) 吗? - wilmol

36

在单元测试中,应该避免使用Guice,因为每个测试应该足够小,以便手动依赖注入(DI)可以管理。通过在单元测试中使用Guice(或任何DI),您正在隐藏一个警告,即您的类变得太大并承担了太多责任。

对于测试引导程序代码和集成测试,则可以为每个测试创建不同的注入器。


11
我不同意。使用Guice,您可以使用@inject注释并注入没有setter或构造函数的字段。这样更易读。那么在这种情况下手动处理依赖关系应该怎么做呢?我更喜欢使用Injector而不是手动Reflection API,因为它首先出现在我的脑海中。 - Michał Króliczek
6
我从未直接向字段注入,而是使用 setter。我几乎从不使用 setter 注入,因为我认为这些方法很丑陋,并且会隐藏类对用户的要求。我尽量只使用构造函数注入。在单元测试中使用 Guice(或任何 DI 工具),你将忽略一个警告:你的类变得越来越臃肿,承担了太多的责任。 - Michael Lloyd Lee mlk
4
你是否倾向于编写“肤浅”的单元测试,即模拟测试对象的直接依赖项?我发现,如果你使用实际存储等编写“全栈”测试,手动创建依赖树的大部分可能会很麻烦。不过,我并不想就哪种测试方法更好展开辩论。 - Daniel Lubarov
4
没有所谓的“更好”,只有“更适合这个使用情况”。 - Michael Lloyd Lee mlk
7
当使用JUnit框架来运行集成测试时怎么办? - Dan Gravell
显示剩余4条评论

13

我认为使用DI会使单元测试代码更简单,我总是在单元测试和集成测试中使用 DI。

没有 DI 一切都感觉很难编写。无论是使用Guice Inject或Spring Autowired,就像下面的测试代码:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/application-context.xml")
public class When_inexists_user_disabled {
    @Autowired
    IRegistrationService registrationService;

    private int userId;

    @Before
    public void setUp() {
        Logger.getRootLogger().setLevel(Level.INFO);
        Logger.getLogger("org.springframework").setLevel(Level.WARN);
        BasicConfigurator.configure();

        userId = 999;
    }

    @Test(expected=UserNotFoundException.class)
    public void user_should_have_disabled() throws UserNotFoundException {
        registrationService.disable(userId);
    }

}

1
个人认为这更难解决,因为我需要查看应用上下文文件来了解正在使用哪个IRegistrationService,它是否正在使用任何模拟或存根以及它们如何设置。 如果一个测试感觉手动编码太困难,那么这表明您可能测试过多或者您的对象可能需要太多才能开始。 - Michael Lloyd Lee mlk
@mlk,使用注释配置时情况远不及糟糕,因为您可以在单个[at]Configuration bean中设置所需的所有内容,包括模拟对象,您可以将其作为内部类。 - AnthonyJClink

7
这取决于使用的JUnit版本。我们的团队已经成功地使用了Junit4,并且现在正在研究JUnit5。
在JUnit5中,我们使用扩展(extension)。
    public class InjectionPoint implements BeforeTestExecutionCallback {

        @Override
        public void beforeTestExecution(ExtensionContext context) throws Exception {

            List<Module> modules = Lists.newArrayList(new ConfigurationModule());

            Optional<Object> test = context.getTestInstance();

            if (test.isPresent()) {
                RequiresInjection requiresInjection = test.get().getClass().getAnnotation(RequiresInjection.class);

                if (requiresInjection != null) {
                    for (Class c : requiresInjection.values()) {
                        modules.add((Module) c.newInstance());
                    }
                }

                Module aggregate = Modules.combine(modules);
                Injector injector = Guice.createInjector(aggregate);

                injector.injectMembers(test.get());
                getStore(context).put(injector.getClass(), injector);
            }

        }

        private Store getStore(ExtensionContext context) {
            return context.getStore(Namespace.create(getClass()));
        }

    }

然后每个测试都使用RequiresInjection注解,该注解可以接受一个内部模块数组来聚合,或者不使用任何参数来使用默认值。

    @RequiresInjection
    public class Junit5InjectWithoutModuleTest {

        @Inject
        private TestEnvironment environment;

        @Test
        public void shouldAccessFromOuterModule() {
            assertThat(environment).isNotNull();
        }

    }

以下是注释:

    @ExtendWith(InjectionPoint.class)
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
    public @interface RequiresInjection {

        Class<? extends Module>[] values() default {};

    }

JUnit5 对我来说还比较新,所以我可能会研究模板,但目前扩展似乎很管用。

在 JUnit4 中,我们使用类似的方法,不同之处在于注入发生在自定义测试运行器的 createTest 方法中,然后每个测试实现 RequiresInjection 接口,该接口有一个 "getModule" 方法。

我也应该向 TestNG 致敬,因为它内置了 Guice 支持。使用起来就像这样简单:

@Guice({SomeObjectModule.class})
    public class MyTest {

        @Inject
        SomeObject someObject;    

    }

6

看一下Guice Berry

我不建议现在使用它(文档真的很糟糕),但是看看他们的方法可以让你清楚地思考在jUnit中应该如何进行DI。


如果您决定使用GuiceBerry,您可以创建带有@TestScoped注释的@Provides函数(http://stackoverflow.com/a/37979254/345648)(或`bind(YourClass.class).in(TestScoped.class);`)。这告诉Guice每个测试只创建一个实例,而不是像@Singleton那样使组件在测试之间重复使用,或者没有注释,每次注入时都会创建一个新实例(可能每个测试有多个实例)。 - Alexander Taylor

2
我发现AtUnit是Guice的一个很好的补充(它甚至处理了模拟框架集成)。
这使得单元测试类非常清晰简洁(从未看到过Injector),并且在适当的情况下,还可以让您在单元测试中使用生产绑定。

如果我没错的话,AtUnit源代码库的最后一次提交是在2008年。 - Hartmut Pfarr

1
我建议使用我最近编写的框架Guice-Behave。它非常简单,只需要两个注释就可以在应用程序的相同上下文中运行测试。您可以在Guice模块内定义您的mocks,这样很容易重复使用它们。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接