Search code examples
javamockingmockitopowermockitointercept

Intercepting real non-static method calls with Mockito


Is there any way, using Mockito or PowerMockito, to intercept calls to non-static methods of an object, or at least of a singleton object?

An example is provided by the following classes:

public class Singleton {

  private static Singleton INSTANCE = null;

  private Singleton(Object parameter) {}

  public static Singleton getInstance(Object parameter) {
    if (INSTANCE == null) {
      INSTANCE = new Singleton(parameter);
    }
    return INSTANCE;
  }

  public String process(String a, String b) {
    return (a + b);
  }

  // Other methods
}

public class Foreign {

  private Foreign() {}

  public static void main(String[] args) {
    System.out.println(Singleton.getInstance(new Object()).process("alpha", "beta"));
  }
}

The Singleton object is created in a Foreign class, outside the control of some test code (not shown above). Neither of these two classes can be modified. The objective is to intercept calls to the non-static process() method in the test code so that, for certain values, a different result is returned, e.g. the call

Singleton.getInstance(new Object()).process("alpha", "beta");

mocked to return "alpha-beta" instead of the expected "alphabeta".

One solution could be intercepting the Singleton.getInstance() method to instantiate a custom subclass of the Singleton, e.g. using

public class SubSingleton extends Singleton {

  public SubSingleton(Object parameter) {
    super(parameter);
  }

  public String process(String a, String b) {
    if ("alpha".equals(a) && "beta".equals(b)) {
      return a + "-" + b;
    }
    return super.process(a + b);
  }
}

Then, calls to the Singleton.process() method would be intercepted as in:

Object parameter = new Object();
PowerMockito.doReturn(new SubSingleton(parameter)).when(Singleton.class, "getInstance", parameter);

However, the Singleton class above only provides a private constructor, so it cannot be extended. Using PowerMockito.whenNew() to return a partial mock (spy) will also not work, since the Singleton class does not provide a no-args constructor.

Can the desired mocking be implemented in any other way? Can it be done for non-singleton classes?


Solution

  • Firstly, you can use whenNew for objects with constructor with some params:

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Singleton.class)
    public class SingletonPrivateNewTest {
    
        @Mock
        Singleton singletonMock;
    
        @Before
        public void setUp() throws Exception {
            PowerMockito.whenNew(Singleton.class)
                    .withAnyArguments()
                    .thenReturn(singletonMock);
        }
    
        @Test
        public void testMockNew() throws Exception {
            Mockito.when(singletonMock.process(anyString(), anyString())).thenReturn("sasa");
            Foreign.main(new String[0]);
        }
    }
    

    Secondly, why not stub getInstance instead of new:

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Singleton.class)
    public class SingletonPrivateNewTest {
    
        @Test
        public void testMockNew() {
            PowerMockito.mockStatic(Singleton.class);
            Singleton singletonMock = Mockito.mock(Singleton.class);
            PowerMockito.when(Singleton.getInstance(any())).thenReturn(singletonMock);
            Mockito.when(singletonMock.process(anyString(), anyString())).thenReturn("sasa");
            Foreign.main(new String[0]);
        }
    }
    

    Thirdly, to intercept the process method:

    • create real singleton
    • create a mock singleton
    • mock static getInstance to return the mock. NOTE: you must call mockStatic after getting real instance.
    • use thenAnswer to check the arguments on process call
      • return desired answer if they match desired pattern
      • else call real method on real singleton
    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Singleton.class)
    public class SingletonPrivateNewTest {
    
        @Test
        public void testMockNew() {
            var singletonReal = Singleton.getInstance(new Object());
            var singletonMock = Mockito.mock(Singleton.class);
            PowerMockito.mockStatic(Singleton.class);
            PowerMockito.when(Singleton.getInstance(any())).thenReturn(singletonMock);
            Mockito.when(singletonMock.process(anyString(), anyString())).thenAnswer((args) -> {
                String a = args.getArgument(0);
                String b = args.getArgument(1);
                if ("alpha".equals(a) && "beta".equals(b)) {
                    return "sasa";
                } else {
                    return singletonReal.process(a, b);
                }
            });
            Foreign.main(new String[0]);
        }
    }
    

    And finally, use a spy instead of a mock

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Singleton.class)
    public class SingletonPrivateNewTest {
    
        @Test
        public void testMockNew() {
            var singletonReal = Singleton.getInstance(new Object());
            var singletonMock = Mockito.spy(singletonReal);
            PowerMockito.mockStatic(Singleton.class);
            PowerMockito.when(Singleton.getInstance(any())).thenReturn(singletonMock);
            Mockito.when(singletonMock.process("alpha", "beta")).thenReturn("sasa");
            // NOTE: real method is called for other args
            Foreign.main(new String[0]);
        }
    }