Or press ESC to close.

Challenges and Benefits of Mocking External Services With Examples

Mar 28th 2023 9 min read
medium
java17.0.6
mockito5.2.0

Mocking external services is a technique in automation testing that enables you to test your code without actually making calls to external services. This technique has a multitude of benefits, including improving the reliability of tests, increasing the speed of test execution, and avoiding test failures caused by external factors.

When your code relies on external services, such as APIs or databases, it can be challenging to test it thoroughly. External services can be unreliable, slow, or simply unavailable, making it difficult to reproduce test scenarios consistently. By mocking these external services, you can create a simulation of the real service that behaves in the same way as the actual service but without any of the limitations or drawbacks.

It all sounds great, but mocking external services can be a complex and challenging task, especially when dealing with complex services or dependencies. Let's see some concrete examples.

Replicating complex behavior

Some external services may have complex or non-deterministic behavior that is difficult to replicate in a mock. For example, a service that uses machine learning algorithms or complex business logic may be difficult to mock accurately.

Let's say we have a service class MainService that depends on an external service ExternalService that performs complex business logic. We want to mock this external service in our tests:

                                     
public class MainService {
    private ExternalService externalService;
                        
    public MainService(ExternalService externalService) {
        this.externalService = externalService;
    }
                        
    public String doSomething() {
        String externalResult = externalService.performComplexBusinessLogic();
        return externalResult + " with main service data";
    }
}
                    

To mock the ExternalService, we can use a mocking framework like Mockito and create a mock object of ExternalService. However, since the behavior of the ExternalService is complex and non-deterministic, we need to specify the behavior of the mock object in our test.

                                     
@Test
public void doSomethingMockedTest() {
    ExternalService externalServiceMock = Mockito.mock(ExternalService.class);
    Mockito.when(externalServiceMock.performComplexBusinessLogic())
            .thenReturn("some complex logic");
    MainService mainService = new MainService(externalServiceMock);
    String result = mainService.doSomething();
    assertEquals(result, "some complex logic with main service data");
}
                    

In this example, we create a mock object of ExternalService using Mockito.mock() and specify the behavior of the mock object using Mockito.when() and Mockito.thenReturn(). We tell the mock object to return a hardcoded string "some complex logic" when performComplexBusinessLogic() method is called.

This allows us to test the behavior of MainService without actually calling the ExternalService. We can test different scenarios by changing the behavior of the mock object, which is much easier and more reliable than testing with the real external service.

When mocking external services to replicate complex behaviors, we need to ensure that the mock accurately reflects the behavior of the real service. Here are some ways that we can deal with replicating complex behaviors of external services when mocking them:

Handling complex data structures

Suppose we have a service that returns a JSON object with a complex data structure. As an example, we are going to use the following simplified object:

                                     
{
    "address": {
        "country": "France",
        "city": "Paris",
        "population": {
            "size": 2161000,
            "birthRate": 12.6,
            "deathRate": 6.4
        }
    }
}
                    

We will use the simplified object since the same principles apply to any object's complexity, and it will make our example more readable.

Now, let's imagine that our main service calls the external service, retreives the city from the object, and uses it with some of its own data.

With Mockito, we can create a mock of our complex data structure:

                                     
ExternalService mockService = Mockito.mock(ExternalService.class);

Map<String, ExternalService.Location> expectedData = new HashMap<>();
expectedData.put("address", new ExternalService.Location(
        "France",
        "Paris",
        new ExternalService.Population(2161000, 12.6, 6.4)
        )
);
                    

And set it up to return this data structure when getComplexDataStructure is called:

                                     
Mockito.when(mockService.getComplexDataStructure()).thenReturn(expectedData);
                    

We then create an instance of MainService, which uses the ExternalService to retrieve data, and we test that the data returned by complexDataStructureUse matches the expected data:

                                     
MainService mainService = new MainService(mockService);
String actualData = mainService.complexDataStructureUse();
                
assertEquals(actualData, "Complex data returned Paris");
                    

Synchronization and consistency

In some cases, the mock service may need to synchronize its behavior with the real service to ensure consistency. This can be difficult to achieve, especially when dealing with asynchronous or distributed services. Let's show an example of how this can be done.

First, we are going to create a mock of our external service:

                                     
ExternalService externalServiceMock = Mockito.mock(ExternalService.class);
                    

Our external service has a getData method that calls an API that returns some age-related data whose behavior we are going to mock:

                                     
ExternalService.AgeData mockData = new ExternalService.AgeData();
mockData.age = 62;
mockData.count = 298219;
mockData.name = "michael";
Mockito.when(externalServiceMock.getData()).thenReturn(mockData);
                    

Next, we are going to set up our real service to retreive data asynchronously:

                                     
ExecutorService executorService = Executors.newSingleThreadExecutor();
CountDownLatch latch = new CountDownLatch(1);
executorService.execute(() -> {
    ExternalService.AgeData realData = null;
    try {
        externalService = new ExternalService();
        realData = externalService.getData();
    } catch (IOException e) {
        e.printStackTrace();
    }
});
                    

Once the real data is retrieved, we are going to synchronize the mock with it:

                                     
synchronizedMock.setData(realData);
                    

And release the latch to signal that the data has been retrieved:

                                     
latch.countDown();
                    

Outside the executor service, we are waiting for the real data to be retrieved:

                                     
latch.await();
                    

And in the end, we are going to use our synchronized mock to test our code:

                                     
ExternalService.AgeData synchronizedData = synchronizedMock.getData();
assertEquals(mockData.getAge(), synchronizedData.getAge());
assertEquals(mockData.getCount(), synchronizedData.getCount());
assertEquals(mockData.getName(), synchronizedData.getName());
                    

In summary, the external service is being mocked and its behavior is being synchronized with the real service to ensure consistency. The mock service is asynchronously retrieving data, and a latch is being used to wait for the real data to be retrieved. Once the real data is retrieved, the synchronized mock is updated with the real data, and the latch is released. Finally, the synchronized mock is used to test the code, ensuring consistency between the mock and the real service.

Security and access control

Authenticating or controlling access to certain external services can be difficult to replicate in a mock environment. Sometimes, the mock service must simulate the authentication or authorization process to effectively mimic the behavior of the actual service.

Let's say we have a class AuthExternalService that communicates with an external service that requires authentication. To mock the authentication behavior, we could do the following steps:

First, we create a mock authentication token:

                                     
String authToken = "abc123";
                    

Then we mock the authentication service to return the mock token:

                                     
when(authService.authenticate(eq("username"), eq("password"))).thenReturn(authToken);
                    

Once mocked, we create the external service client with the mock authentication service and call the service with the mock token:

                                     
AuthExternalService client = new AuthExternalService(authService);
String response = client.callExternalService(authToken);
                    

Now we can verify that the authentication service was called with the correct parameters:

                                     
verify(authService).authenticate(eq("username"), eq("password"));
                    

And that the external service was called with the mock token:

                                     
assertEquals(response, "Response from external service");
                    

Note that this is just an example implementation, and the exact interface and implementation of the AuthenticationService will depend on the specific authentication requirements of the external service being mocked.

Maintenance and updates

As the real external service changes over time, the mock service may need to be updated to reflect these changes. This can be time-consuming and difficult, especially when dealing with complex or poorly documented services. Let's look at a small example.

Suppose we have a class called UserService which makes a call to an external API to retrieve user information:

                                     
public class UserService {

    private RestTemplate restTemplate;
                        
    public UserService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
                        
    public User getUser(String userId) {
        String url = "https://api.example.com/users/" + userId;
        User user = restTemplate.getForObject(url, User.class);
        return user;
    }
}
                    

To mock the API call, we could do something like this:

                                     
@Test
public void getUserTest() {
    RestTemplate restTemplate = Mockito.mock(RestTemplate.class);
                    
    User expectedUser = new User("123", "John Doe");
    Mockito.when(restTemplate.getForObject("https://api.example.com/users/123", User.class))
            .thenReturn(expectedUser);
                    
    UserService userService = new UserService(restTemplate);
    User actualUser = userService.getUser("123");
                    
    assertEquals(actualUser, expectedUser);
}
                    

Now suppose the external API changes its endpoint from https://api.example.com/users/ to https://api.example.com/v2/users/. We need to update our UserService to reflect this change.

The URL in the getUser method would change into:

                                     
String url = "https://api.example.com/v2/users/" + userId;
                    

And the Mockito.when statement in the test to:

                                     
Mockito.when(restTemplate.getForObject("https://api.example.com/v2/users/123", User.class))
.thenReturn(expectedUser);
                    

When dealing with complex or non-deterministic behavior, it can be helpful to use advanced mocking frameworks such as Mockito or EasyMock, which allow developers to specify complex behavior for mocks.



When dealing with synchronization, consistency, and security challenges, it is important to thoroughly test and debug the mock service to ensure that it accurately replicates the behavior of the real service.



As the real external service changes over time, it is important to keep the mock service up-to-date to ensure that it accurately reflects the behavior of the real service.

As always, all code examples can be found on our GitHub repository. See you soon.