jmockit / jmockit1

Advanced Java library for integration testing, mocking, faking, and code coverage
Other
461 stars 239 forks source link

Tested fullyInitialized instances with interfaces in constructor need to define mocks #696

Open Saljack opened 3 years ago

Saljack commented 3 years ago

Please provide the following information:

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();
   }
rliesenfeld commented 3 years ago

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.

Saljack commented 3 years ago

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;
}
rliesenfeld commented 3 years ago

"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.

Saljack commented 3 years ago

"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).