Sorry for starting a discussion and then not responding for so long.
December has been busier than expected.
Anyway I've been digging a little deeper into Guice and Jukito and also discussed my proposed changes and this discussion with a couple of coworkers.
I guess there is no perfect solution.
Extracting an Abstract runner class and then providing two concrete runners is simple. But it should be discouraged to write too many extension to the abstract runner.
I belive that the runner is a detail of the test which developer most of the time don't pay enough attention to. And the more runners there are the harder it will be to keep track of how each runner configures the injector which is used for running the test.
Therefore the design of the mechanism which allows to inherit an injector configuration and maybe modifying this configuration seems of much greater interest to me
In
https://groups.google.com/d/topic/jukito/vDdcNKSsVCw/discussion Julian provided a patch to Jukito which allows to install Modules with an annotation. After writing several sample tests and some more discussions I came to the conclusion, that the Annotation approach is sufficient. Compared with my above proposal the amount of code which can be saved is neglectable. Therefore I will withdraw my proposed solution in favor of the solution by Julian.
In my current project I happen to stumble across the very rare case where I wanted to overwrite a binding from our common test module. Again I tried different approaches to solve this. One was refactoring the common module. It felt kind of funny because I was forced to ad a special sub classing for this single test. This didn't feel right. The class hierarchy suddenly had knowledge about the implementation of a specific sub class. In the current Jukito approach one can easily use Modules.overwrite() to replace one or more bindings for a specific test class. Another approach was to write a test specific module which does nothing more than to overwrite some bindings and then installing the resulting module. Which is a lot of code for not much action. The last approach was to introduce a second annotation which allows to overwrite inherited modules. This also didn't feel right. Adding a special annotation for every use case cannot be the solution. At the end one would just reinvent the API except of using classes with methods one would only use annotations.
A second shortcoming of the annotation based approach (and my proposed approach as well) is that it is not possible to decide which module should be use. Jukito provided two abstract modules which can be used to configure the injector for testing. TestModule and JukitoModule. The test will behave differently depending on which module is extended in the test class.
Finally I came up with a new approach:
Modules are basically a callback function. Guice will call configure(Binder) whenever it needs the informations stored in an Module.
One could write a Module which is configurable from the outside using the Guice Elements SPI. This would allow multiple actors to have an influence of the configuration which is stored in a module. In our scenario of test class hierarchies this would mean that a subclass could change the inherited configuration from a superclass. Such a configurable module would also be capable of behaving like a TestModule or like a JukitoModule based in its configuration.
I am well aware that writing such a module involves a lot more work than the annotation based approach. Still I think it is a very interesting approach and would be willing to invest some time into it.
I will add some example code to further outline what I like to propose:
public class BaseTestCase {
@JukitoModuleProvider
public static JukitoModule commonModules(JukitoModule module) {
module.bind(InterfaceA.class).to(AImpl.clas);
module.install(new DefaultModule());
return module;
}
@Befor
public void setUp(InterfaceA a) { ... }
/* common test methods. */
}
// This class inherits the module unchanged from BaseTestCase
public class Test1 extends BaseTestCase {
@Test
public void fooTest(InterfaceA a) { ... }
}
// This class does the same as Test1. It is just more verbose
public class Test1A extends BaseTestCase {
@JukitoModuleProvider
public static JukitoModule commonModules(JukitoModule module) {
return module;
}
@Test
public void fooTest(InterfaceA a) { ... }
}
// This class extends the module from BaseTestCase
public class Test2 extends BaseTestCase {
@JukitoModuleProvider
public static JukitoModule specificModules(JukitoModule module) {
module.bind(InterfaceB.class).toProvider(BProvider.class);
module.overwrite(InterfaceA.class).with(AImplMock.class);
return module;
}
@Test
public void fooTest(InterfaceB b) { ... }
}
// This class ignores the module from BaseTestCase
public class Test2 extends BaseTestCase {
@JukitoModuleProvider
public static JukitoModule specificModules() {
JukitoModule module = new JukitoModule();
module.bind(InterfaceC.class).to(CImpl.class);
return module;
}
@Test
public void fooTest(InterfaceC c) { ... }
}
// This class does the same as Test2. But the runner can't tell by the
// method signature that the module provided by the super class is ignored.
public class Test2A extends BaseTestCase {
@JukitoModuleProvider
public static JukitoModule specificModules(JukitoModule module) {
module = new JukitoModule();
module.bind(InterfaceC.class).to(CImpl.class);
return module;
}
@Test
public void fooTest(InterfaceC c) { ... }
}