Nital
Nital

Reputation: 6114

Best practices on how to test a void returning method in JUnit and Spring?

I am not sure if I am testing a void returning method the correct way and also if my class-under-test (cut) requires any change in order to make it 100% testable and bug-proof.

I am seeing NullPointerException while executing the test because loginOperations is not getting set.

Error:

java.lang.NullPointerException
    at com.demo.service.LoginService.doLogin(LoginService.java:40)
    at com.demo.service.LoginServiceTest.doLogin(LoginServiceTest.java:25)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

LoginService.java

@Service
public class LoginService {

    @Autowired
    private ILoginOperations loginOperations;

    public void doLogin(HttpServletRequest request, String result) {
        LoginDTO loginDTO = new LoginDTO(request.getParameter("username"), result);
        loginOperations.doLogin(loginDTO);
    }
    
}

LoginServiceTest.java

public class LoginServiceTest {

    private LoginService instance = new LoginService();

    ILoginOperations loginOperations = Mockito.mock(ILoginOperations.class);
    HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
    String result = "some string";

    @Test
    public void doLogin() {
        when(request.getParameter("username")).thenReturn("johndoe");
        instance.doLogin(request, result); //throws NPE while calling loginOperations.doLogin() because 
        assertNotNull(instance); //IS THIS THE CORRECT WAY TO TEST A VOID RETURNING METHOD ???
    }

}

Now, there are 2 ways to fix the test.

  1. I can fix the class-under-test by adding a setter method for loginOperations class and call that setter method in the test
  2. Change @Test public void doLogin() { to @Test(expected = Exception.class) public void doLogin() {

Not sure which one is the best practice above and why.

Another Question:

Other question that I have is how to assert on a method that returns nothing. There is something like verify() but not sure how to use it.

Upvotes: 1

Views: 347

Answers (2)

Pankaj
Pankaj

Reputation: 2708

1.You can fix the test case by adding setter method in LoginService or you can use constructor injection like -

@Autowired 
public LoginService(ILoginOperations loginOperations) {
    this.loginOperations = loginOperations;
}
  1. Validating exception as @Test(expected = Exception.class) public void doLogin() is certainly not a good idea as doLogin method does not throw exception in normal circumstance.

The better way to test method with void return type is using verification API (example - mockito verification API example). You can also use Mockito's ArgumentCaptor to capture argument and assert state of that argument, along with verification API as -

@RunWith(MockitoJUnitRunner.class)
public class LoginServiceTest {

    @Captor
    private ArgumentCaptor<LoginDTO> captor;
    @Mock
    private ILoginOperations loginOperations;
    @Mock
    private HttpServletRequest mockServletRequest;
    @InjectMocks
    private LoginService loginService;

    @Test
    public void validateLogin() {
        when(mockServletRequest.getParameter("username")).thenReturn("mock_user_name");
        loginService.doLogin(mockServletRequest, "mock_result");

        verify(loginOperations).doLogin(captor.capture());
        LoginDTO expectedLoginDTO = captor.getValue();
        
        assertThat(expectedLoginDTO.getResult(), is("mock_result"));
        assertThat(expectedLoginDTO.getUsername(), is("mock_user_name"));
    }
}

There is an excellent article from Martin Fowler about this method of testing - Mocks Aren't Stubs

Upvotes: 1

actually you should create a constructor for your LoginService that gets the ILoginOperations, in this way you can create the LoginService in your test class and pass the mocked ILoginOperations as parameter, all this stuff should be done in a @Before method.

Or you can try with @InjectMocks for your LoginService and have your ILoginOperations annotated as @Mock.

Upvotes: 1

Related Questions