Search code examples
javaunit-testingmockitopowermockito

How to mock a method which is being called from class level object


I am writing unit test for a class A, I want to mock a method but that method is benign called from a class level object, How i will mock this.

Let me explain it from example

Class A which is under test.

public class ClassA {
    ClassB objectOfB = new ClassB();
    public int add(int a, int b) {
        int addition = objectOfB.performCalculation(a,b);
        return addition;
    }
}

Class B, which has some business logic.

  public class ClassB {
    public int performCalculation(int a, int b) {
        int c = a+b;
        System.out.println("I am not mocked, I am actual call");
        System.out.println("Returning " + c + " From ClassB");
        return c;
    }
  }

Test Written

import static org.junit.Assert.*;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({ClassA.class, ClassB.class})
public class ClassATest {
    @InjectMocks
    ClassA objA = new ClassA();

    @Test
    public void testAddFromClassA() throws Exception {
        ClassB objB = Mockito.mock(ClassB.class);
        Mockito.when(objB.performCalculation(5, 10)).thenReturn(15);
        int result = objA.add(5, 10);
        assertEquals(result, 15);
    }

}

Test Result:

This test is pass, but it is not mocking ClassB's method, instead it is performing actual call.


Requirement:

While writing test, i want to mock line : objectOfB.performCalculation(a,b); from class A, but as you can see object of classB() is created on class level.

How I can mock this?

What i should write in my test class.

enter image description here


Solution

  • Mock the initialization of the class so that the mock is used when exercising the test

    @RunWith(PowerMockRunner.class)
    @PrepareForTest({ClassA.class}) //prepare the class creating the new instance of ClassB for test, not the ClassB itself.
    public class ClassATest {
        @Test
        public void testAddFromClassA() throws Exception {
            int expected = 15;
            ClassB objB = Mockito.mock(ClassB.class);
            Mockito.when(objB.performCalculation(5, 10)).thenReturn(expected);
    
            //mocking initialization of ClassB class withing ClassA class
            PowerMockito.whenNew(ClassB.class).withNoArguments().thenReturn(objB);
    
            ClassA objA = new ClassA();
    
            //Act
            int actual = objA.add(5, 10);
    
            //Assert
            assertEquals(expected, actual);
        }
    }
    

    Reference How to mock construction of new objects

    Now with that said, ideally the target class should follow explicit dependency principle via constructor injection

    public class ClassA {
        final ClassB objectOfB;
    
        public ClassA(ClassB objectOfB) {
            this.objectOfB = objectOfB;
        }
    
        public int add(int a, int b) {
            int addition = objectOfB.performCalculation(a,b);
            return addition;
        }
    }
    

    The allows the class to explicitly state what it depends on to perform its designed function.

    It also allows for inversion of control and loose coupling, which makes the class more flexible to maintain and test

    @RunWith(PowerMockRunner.class)
    public class ClassATest {
        @Test
        public void testAddFromClassA() throws Exception {
            int expected = 15;
            ClassB objB = Mockito.mock(ClassB.class);
            Mockito.when(objB.performCalculation(5, 10)).thenReturn(expected);
    
            ClassA objA = new ClassA(objB);
    
            //Act
            int actual = objA.add(5, 10);
    
            //Assert
            assertEquals(expected, actual);
        }
    }
    

    Just because PowerMockito allows for the mock construction of new objects does not mean that we should.

    If proper design principles are followed, then there really is no need for such hacks.