javaunit-testingdependency-injectionjmockit

How to call real methods in class instances annotated with @Injectable in JMockit?


I'm looking for a way in JMockit to inject the private fields inside a class while maintaining the ability to trigger the real methods. I use @Injectable and @Tested offered by JMockit. But somehow after that the injected instance is not able to call the real method.

Example test:

public class TestClass {
    public static class DoSomething {
        private Call callee;

        public void execute() {
            callee.call();
        }
    }

    public static class Call {
        public void call() {
            System.out.println("real");
        }
    }

    @Tested DoSomething doSomething;
    @Injectable Call call;

    // nothing happens
    @Test
    public void testRealCall() {
        doSomething.execute();
    }

    // invocation doesn't help either
    @Test
    public void testRealCallSecondTry() {
        new MockUp<Call>() {
            @Mock
            @SuppressWarnings("unused")
            public void call(Invocation inv) {
                inv.proceed();
            }
        };
        doSomething.execute();
    }

    // this works, but requires redundant methods
    @Test
    public void testRealCallThirdTry() {
        new MockUp<Call>() {
            @Mock
            @SuppressWarnings("unused")
            public void call() {
                System.out.println("real");
            }
        };
        doSomething.execute();
    }

    @Test
    public void testFakeCall() {
        new MockUp<Call>() {
            @Mock
            @SuppressWarnings("unused")
            public void call() {
                System.out.println("fake");
            }
        };
        doSomething.execute();
    }
}

Here DoSomething wraps the Call instance, which provides a way to print a message. The ideal output of the four test cases would be:

real
real
real
fake

However the actual scenario is that only 3 and 4 worked, printing:

real
fake

This shows if an instance is created using @Injectable. It's not able to directly call the original method without copying and pasting the old method body to the mocked version. That seems really awkward. Is there a workaround of this?


Solution

  • My understanding is that if you use @Injectable you just get an empty mock and then you can no longer call the original method.

    The workaround that I would use is to do the injection "manually" like this:

    public class TestClass {
      public static class DoSomething {
        private Call callee;
    
        public void execute() {
          callee.call();
        }
      }
    
      public static class Call {
        public void call() {
          System.out.println("real");
        }
      }
    
      @Tested DoSomething doSomething;
      //@Injectable Call call;
    
      // nothing happens
      @Test
      public void testRealCall() {
        Deencapsulation.setField(doSomething, "callee", new Call());
        doSomething.execute();
      }
    
      // invocation doesn't help either
      @Test
      public void testRealCallSecondTry() {
        new MockUp<Call>() {
          @Mock
          @SuppressWarnings("unused")
          public void call(Invocation inv) {
            inv.proceed();
          }
        };
        Deencapsulation.setField(doSomething, "callee", new Call());
        doSomething.execute();
      }
    
      // this works, but requires redundant methods
      @Test
      public void testRealCallThirdTry() {
        new MockUp<Call>() {
          @Mock
          @SuppressWarnings("unused")
          public void call() {
            System.out.println("real");
          }
        };
        Deencapsulation.setField(doSomething, "callee", new Call());
        doSomething.execute();
      }
    
      @Test
      public void testFakeCall() {
        new MockUp<Call>() {
          @Mock
          @SuppressWarnings("unused")
          public void call() {
            System.out.println("fake");
          }
        };
        Deencapsulation.setField(doSomething, "callee", new Call());
        doSomething.execute();
      }
    }