Search code examples
mockitojunit5liferay-7junit-jupiter

How to mock or prepare test methods in case we called private method inside the method?


While writing a testCase for Controller class,The private method that is getServiceContext(). has different object because one we are passing serviceContext from testclass and other object inside the controller class itself call itself.Due to this Foo object is null. how to resolve this.

public class Controller {

@Refernce
private FooService fooService;

public CustomData getDetails(String id){

      Foo foo = fooService.getFoo(id ,**getServiceContext()**); 
         //getServiceContext() is different object
    System.out.println("foo data>>>> "+foo); // **Throwing null pointer exceptions** 

      CustomData customData = new CustomData();
      customData.setStudentName(foo.getName);
      customData.setStudentName(foo.getId);
      ...
      ...
      ...
    return  customData;

}

private ServiceContext getServiceContext() {

 ServiceContext serviceContext = new ServiceContext();
    serviceContext.setCompanyId(context..);
    serviceContext.setUserId(context..);
    ...
    ....    
retrn serviceContext;

}

}


public class ControllerTest {

@InjectMocks
private Controller controller;

@Mock
private FooService fooService;

private Foo foo;

@BeforeEach
public void setUp() throws PortalException {

foo = mock(Foo.class);

}

@Test
public void getDetailsTest() throws Exception {

    ServiceContext **serviceContext** = new ServiceContext();
    serviceContext.setCompanyId(context..);
    serviceContext.setUserId(context..);
    ...
    ....    
    Mockito.when(fooService.getFoo("testId",serviceContext)).thenReturn(foo);

    System.out.println("Service context>>>> "+**serviceContext**); // different serviceContext object
    CustomData customData = controller.getDetails("testId");

    Assertions.assertThat(ss).isNotNull();
}

}


Solution

  • There are multiple ways to do that.

    First, we can mock with anyOf(Type.class), that will actually match object type rather than value.

    Mockito
     .when(fooService.getFoo(Mockit.eq("testId"), Mockito.any(ServiceContext.class)))
     .thenReturn(foo);
    

    this will work as expected and return the desired value.

    Additionally, if you want to check with what data serviceContext object is being passed as arg in service method, (as we just checked object type rather than value), We can use ArgumentCaptor for that.

    It basically captures argument data which is being passed in the method call.

    let's create ArgumentCaptor for service context

    @Mock
    private FooService fooService;
    
    @Captor
    private ArgumentCaptor<ServiceContext> captor;
    

    Now, let's capture the argument during verification.

    Mockito.verify(fooService).getFoo(Mockit.eq("testId"), captor.capture());
    
    Assertions.assertEquals("value of x in context", captor.getValue().getX());
    

    Basically here, captor.getValue() returns service context object which is being passed. So, you can verify all data you want to validate in that object.

    Alternate, Approach would be Spy which will basically spy on the class under test and you can control the behavior of private methods in test class itself.

    To do that, we need to add @Spy annotation along with @InjectMocks on test class.

    @Spy
    @InjectMocks
    private Controller controller;
    

    Now, you can mock the private method and return the expected value.

    Mockito.doReturn(serviceContextValue).when(controller).getServiceContext();
    

    and use that object for mocking fooService.

    Mockito.verify(fooService).getFoo("testId", serviceContextValue);
    

    But when using Spy, don't forget to write unit test for private method as it's mocked, it's business logic will not be tested by above test cases. that's a reason, it's not recommended way.

    I would suggest using ArgumentCaptor approach.