Software Engineering | MockBean with Spring Boot

Most of us wants to write test classes for testing our APIs. The goal is simple, to make sure if, provided a given input, our API is behaving as expected or not. Doing this is simple, we prepare a sample input (most of the time by hard coding it) , as expected by our API as parameter and we call the API passing the self prepared input as argument. Once the API is executed, we match its actual output with expected output. If both matches, we say our API has passed the test, else it has failed.

But APIs, in real scenarios, aren’t straight forward. They have several dependencies like dependency on other services, database connectivities, CRUD operations and in some cases, integration with external APIs. Among them some dependencies are within the scope of the application like internal services, utils, helpers whereas others are out of scope of the application like database , external API integration, etc.

An integration test case is always intended to test behavior of an API inclusive of all its dependencies. If an API is internally using 30 other services and has integration with 2 third party APIs, then an integration test would expect the API under test to execute with all of its dependencies and generate expected response.

Out of scope dependencies doesn’t always behave as expected. An external API can sometime misbehave because their server might be down or something else. Likewise, a database can also get disconnected because of too many connections or something else. This makes our test vulnerable. They cannot always pass.

To avoid these scenarios we don’t depend on any external service while writing test cases. So we mock those services and their response.

No alt text provided for this image

@MockBean annotation of spring boot test framework mocks the services in our application. When test class is run, an application context is configured. Upon starting, mock version of service is loaded into context. Now when we will hit the API in test, API will call some service which is supposed to hit an external API, but instead it will hit the mocked version of that API.

Using Mockito we can configure when an external API is hit, what should it return.

For example:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/api")
public class SomeController {

    @Autowired SomeService someService;

    @RequestMapping(method = RequestMethod.GET,value = "/length")
    public int getLength(@RequestParam String input){
        return someService.method(input);
    }

}

@Service
class SomeService{

    @Autowired SomeThirdPartyService someThirdPartyService;

    public int method(String input){
        return someThirdPartyService.externalApiMethod(input);
    }

}


@Service
class SomeThirdPartyService{
    public int externalApiMethod(String input){
        return input.length();
    }
}

In the above code we have a controller which provides an API named getLength which takes input from request param. It internally uses some service which calls some third party service. Actual behaviour of this third party service is to take a string as a parameter and return its length.

So if we write an integration test for getLength api without mocking any service, it can be shown below :

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@SpringBootTest(
        classes = {MainClass.class},
        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@RunWith(SpringJUnit4ClassRunner.class)
public class SomeControllerTest {

    @Autowired SomeController someController;
   
    @Test
    public void testGetLengthApi(){
        int length = someController.getLength("input");
        Assert.assertEquals(5, length);
    }
}

Above test will pass if third party service works as expected. But that cannot be guaranteed and if it does not give response, our test will fail. To remove this dependency we will mock third party service using MockBean and will configure what should third party service return whenever invoked

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@SpringBootTest(
        classes = {MainClass.class},
        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@RunWith(SpringJUnit4ClassRunner.class)
public class SomeControllerTest {

    @Autowired SomeController someController;

    @MockBean SomeThirdPartyService someThirdPartyService;

    @Test
    public void testGetLengthApi(){
        Mockito.when(someThirdPartyService.externalApiMethod(Mockito.anyString())).thenReturn(2);
        int length = someController.getLength("input");
        Assert.assertEquals(5, length);
    }

}

Above test case will fail, because we have mocked third party service and configured it to return 2 whenever externalApiMethod is called. Now even if length of input string is 5, API will return 2. Hence our mock bean is successful.

To write successful test case with mock, we can configure the expected response of third party service as below:

@Test
public void testGetLengthApi(){
    String input = "input";
    int expectedLength = input.length();

    Mockito.when(someThirdPartyService.externalApiMethod(Mockito.anyString())).thenReturn(expectedLength);
    
int actualLength = someController.getLength(input);
    Assert.assertEquals(expectedLength, actualLength);

}

Above test case will pass, because we have mocked third party service and configured it to return actual length of input whenever externalApiMethod is called, whether it responds or not.

Intuitive and condense writing! Waiting for more articles like this :)

Informative indeed!! Liked it. :)

Very insightful and well written.

Precise and compact...nice job!!

To view or add a comment, sign in

Others also viewed

Explore content categories