Tested fullyInitialized instances with interfaces in constructor need to define mocks
Please provide the following information:
-
Version of JMockit that was used: 1.49
-
Description of the problem or enhancement request: I have class with many dependencies and I use injectiontion by constructor. I want to test one method which needs only some dependencies and other dependencies can be null. I think it is a good place where I can use fullyInitialized. It works greate with classes because these classes are initialized automaticaly but I use interfaces and JMockit is not able to instate these interfaces. But I do not need this interfaces and null are ok for my test. I do not want to create an
@Injectablefield for all dependencies because it makes no sense to me if I don't want to use it. I tried to play withProviding an interface resolution method(by the way it should be documented in JMockit documentation) but it does not help because some interfaces are runtime generated (Spring Data Repository). Here is an example how I tried it:
@Tested
Class<?> interfaceResolution(Class<?> interfaceClass) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(true);
for (BeanDefinition bd : scanner.findCandidateComponents("my.package")) {
try {
Class<?> cl = Class.forName(bd.getBeanClassName());
if (interfaceClass.isAssignableFrom(cl)) {
return cl;
}
} catch (ClassNotFoundException ex) {
// Do nothing
}
}
return null;
}
So I checked JMockit code and the commit b77f430 Changed the injection behavior into @tested(fullyInitialized) objects, so that non-required fields that cannot be injected don't cause failure (are left null) change this behavior.
When I define a @Tested(fullyInitialized=true) field or parameter and some dependency is an interface and this interface isn't defined as @Injectable then the Tested instance is null without any warning (at least there should be warning that there is missing @Tested or @Injectable for the interface). But I think that a better solution is let the dependency null.
Example:
interface InterfaceDependency { }
class ClassWithInterfaceInConstructor {
ClassWithInterfaceInConstructor(InterfaceDependency someValue) { }
void methodWhatNeedToBeTest() { }
}
@Test
public void instantiateClassWithInterfaceInConstructor(@Tested(fullyInitialized = true) ClassWithInterfaceInConstructor cut) {
assertNotNull(cut);
cut.methodWhatNeedToBeTest();
}
Now the instantiateClassWithInterfaceInConstructor fails because the parameter cut is null. If I want to work it I have to write the test like:
@Test
public void instantiateClassWithInterfaceInConstructor(@Tested(fullyInitialized = true) ClassWithInterfaceInConstructor cut, @Injectable InterfaceDependency unnecessary) {
assertNotNull(cut);
cut.methodWhatNeedToBeTest();
}
Yes, the interface resolution method feature with @Tested needs to be documented.
However, I am not convinced that using null to inject constructor parameters of interface types is a good idea. Constructor injection is inferior to field injection. I am aware that the Spring framework documentation recommends the use of constructor injection, but that's only because (IMO) they don't want to concede that Java EE/CDI got it right with (annotated) field injection.
I don't see a difference between a null in field injection and a null in constructor injection. I think that JMockit should support both. JMockit has support for null in constructor but only for instances without @Tested annotation so why not enable it for them too.
See this from TestedClassWithFullDITest.java :
static class ClassWithUnsatisfiableConstructor { ClassWithUnsatisfiableConstructor(@SuppressWarnings("unused") int someValue) {} }
static class ClassWithFieldToInject { ClassWithUnsatisfiableConstructor dependency; }
@Test
public void instantiateClassWithFieldToInjectWhoseTypeCannotBeInstantiated(@Tested(fullyInitialized = true) ClassWithFieldToInject cut) {
assertNotNull(cut);
assertNull(cut.dependency);
}
There are another ways how to do it. There should be another property in @Tested annotation for example nullInject with default false and than you can enable it as @Tested(fullyInitialized = true, nullInject = true). Or instead null injections there can be mocks (but I am not able to find an easy way how to programmatically create these mocks) and then I can create a interfaceResolution method. Something like:
@Tested
Class<?> interfaceResolution(Class<?> interfaceClass) {
return JMockit.createMock(interfaceClass);
}
I use the constructor injection because class with this injection type are easier to create and test. I don't have to use a third party testing framework like your great JMockit but I can create it easy in a simple unit test. I lose this possibility if I use the field injection with private fields. I create fields as private final and then I use Lombok with @RequiredArgConstructor annotation which generates constructor with these fields.
@RequiredArgsConstructor
class ClassWithConstructorInjection {
private final DependecyField dependencyField;
}
"create it easy in a simple unit test" is not the reality in typical real-world projects. The one I currently develop has lots of Spring DI beans having multiple dependencies on other beans, which in turn have their own injected dependencies. Wiring all that manually would be awful.
"create it easy in a simple unit test" is not the reality in typical real-world projects. The one I currently develop has lots of Spring DI beans having multiple dependencies on other beans, which in turn have their own injected dependencies. Wiring all that manually would be awful.
I completly agree with you but then we can discuss if the class is good designed. But you still have the opportunity to do it. I would like more discuss on the problems from this issue. Returning null for fullyInitialized @Tested where is missing one dependency is definitely bug. There should be at least error message (because user don't know what happened without checking JMockit code).