Guice中的覆盖绑定

159

我刚刚开始使用Guice,我可以想到一个用例就是在测试中我只想覆盖单个绑定。我认为我想使用生产级别的其他绑定来确保所有东西都正确设置并避免重复。

所以,想象一下我有以下模块:

public class ProductionModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(ConcreteA.class);
        binder.bind(InterfaceB.class).to(ConcreteB.class);
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
}

在我的测试中,我只想覆盖InterfaceC,同时保留InterfaceA和InterfaceB不变,因此我希望有类似以下的内容:

Module testModule = new Module() {
    public void configure(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
};
Guice.createInjector(new ProductionModule(), testModule);

我也尝试了以下方法,但没有成功:

Module testModule = new ProductionModule() {
    public void configure(Binder binder) {
        super.configure(binder);
        binder.bind(InterfaceC.class).to(MockC.class);
    }
};
Guice.createInjector(testModule);

有没有人知道我想要的是否可行,或者我完全走错了方向?

--- 进一步地: 如果我在接口上使用@ImplementedBy标记,然后在测试案例中提供一个绑定,似乎可以实现我想要的效果。当接口和实现之间存在1-1映射关系时,这种方法非常有效。

此外,经与同事讨论后,我们似乎将走向覆盖整个模块并确保我们正确定义了自己的模块的路线。不过这似乎可能会导致问题,例如绑定错误放置在模块中并需要移动,从而可能会破坏大量测试,因为绑定可能无法再被覆盖。


10
像短语“barking up the wrong tree”一样:D - Boris Pavlović
5个回答

172
这可能不是你要找的答案,但如果你正在编写单元测试,那么你可能不应该使用注入器,而应该手动注入模拟对象或假对象。

另一方面,如果你真的想要替换单个绑定,你可以使用 Modules.override(..):

public class ProductionModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(ConcreteA.class);
        binder.bind(InterfaceB.class).to(ConcreteB.class);
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
}
public class TestModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
}
Guice.createInjector(Modules.override(new ProductionModule()).with(new TestModule()));

详细信息请参见模块文档

但是,正如Modules.overrides(..)的javadoc建议的那样,您应该设计您的模块,使其不需要覆盖绑定。在您给出的例子中,您可以通过将InterfaceC的绑定移动到单独的模块中来实现这一点。


9
谢谢,阿尔伯特。这让我在实现想做的事情方面向前迈进了一步。但是这还没有发布到生产环境!而且这是为了集成测试,而不是单元测试,所以我需要确保其他所有内容都被正确构建。 - tddmonkey
1
我已经在代码中添加了一个具体的示例。这样是否能够让你更进一步呢? - albertb
1
除非我错了,否则 ovveride 在这样做时会失去正确的 Stage(即始终使用 DEVELOPMENT)。 - pdeschen
1
但是如果你正在编写单元测试,你可能不应该使用注入器,而应该手动注入模拟或虚假对象。>>>我不理解这个。封装创建的整个思想就是将其保留在一个地方。如果手动注入而不是使用注入器,则如果构造函数发生更改,则会出现问题,您需要在所有手动创建的测试类中传播它。
- MaatDeamon
6
大小确实很重要。当您的依赖性图增长时,手动处理连接可能会非常痛苦。而且,当连接更改时,您需要手动更新所有手动连接的位置。覆盖允许您自动处理这一点。 - yoosiba
3
这是Guice 3中的一个错误,我已经在Guice 4中修复了它。 - Tavian Barnes

12

为什么不使用继承?您可以在overrideMe方法中覆盖特定的绑定,同时将共享实现留在configure方法中。

public class DevModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(TestDevImplA.class);
        overrideMe(binder);
    }

    protected void overrideMe(Binder binder){
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
};

public class TestModule extends DevModule {
    @Override
    public void overrideMe(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
}

最后,按照以下方式创建您的注入器:

Guice.createInjector(new TestModule());

6
@Override似乎不起作用,特别是在对提供某些东西的方法进行注释@Provides时。 - Sasanka Panguluri

6

如果您不想改变您的生产模块,并且您有一个类似于默认的Maven项目结构,如下所示:

src/test/java/...
src/main/java/...

您可以在测试目录中使用与原始类相同的包创建一个名为ConcreteC的新类。Guice将会把InterfaceC绑定到您的测试目录中的ConcreteC,而所有其他接口将绑定到您的生产类。

1

在不同的设置中,我们定义了多个活动并分别放置在不同的模块中。被注入的活动位于一个Android库模块中,在AndroidManifest.xml文件中有自己的RoboGuice模块定义。

该设置如下所示。在库模块中有以下定义:

AndroidManifest.xml:

<application android:allowBackup="true">
    <activity android:name="com.example.SomeActivity/>
    <meta-data
        android:name="roboguice.modules"
        android:value="com.example.MainModule" />
</application>

然后我们注入了一个类型:
interface Foo { }

一些 Foo 的默认实现:

class FooThing implements Foo { }

MainModule为Foo配置了FooThing的实现:

public class MainModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Foo.class).to(FooThing.class);
    }
}

最后,一个消耗Foo的Activity:
public class SomeActivity extends RoboActivity {
    @Inject
    private Foo foo;
}

在使用 Android 应用程序模块时,我们希望使用 SomeActivity,但出于测试目的,注入我们自己的 Foo
public class SomeOtherActivity extends Activity {
    @Override
    protected void onResume() {
        super.onResume();

        Intent intent = new Intent(this, SomeActivity.class);
        startActivity(intent);
    }
}

有人可能会主张将模块处理暴露给客户端应用程序,但是我们需要大多数情况下隐藏被注入的组件,因为Library Module是一个SDK,暴露部分具有更大的影响。

(请记住,这是为了测试,所以我们知道SomeActivity的内部情况,并且知道它消耗了一个(package visible) Foo。)

我发现的方法很有意义;使用建议的覆盖测试

public class SomeOtherActivity extends Activity {
    private class OverrideModule
            extends AbstractModule {

        @Override
        protected void configure() {
            bind(Foo.class).to(OtherFooThing.class);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RoboGuice.overrideApplicationInjector(
                getApplication(),
                RoboGuice.newDefaultRoboModule(getApplication()),
                Modules
                        .override(new MainModule())
                        .with(new OverrideModule()));
    }

    @Override
    protected void onResume() {
        super.onResume();

        Intent intent = new Intent(this, SomeActivity.class);
        startActivity(intent);
    }
}

现在,当启动SomeActivity时,它将获得OtherFooThing作为其注入的Foo实例。
这是一个非常具体的情况,在我们的情况下,OtherFooThing用于内部记录测试情况,而默认情况下,FooThing用于所有其他用途。
请记住,我们在单元测试中使用了#newDefaultRoboModule,它运行得非常顺利。

1
你想使用Juckito,在这里你可以为每个测试类声明自定义配置。
@RunWith(JukitoRunner.class)
class LogicTest {
    public static class Module extends JukitoModule {

        @Override
        protected void configureTest() {
            bind(InterfaceC.class).to(MockC.class);
        }
    }

    @Inject
    private InterfaceC logic;

    @Test
    public testLogicUsingMock() {
        logic.foo();
    }
}

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