the/experts. Blog

Cover image for Mastering Mockito's MockedConstruction feature
Maik Kingma
Maik Kingma

Posted on

Mastering Mockito's MockedConstruction feature

Mockito's MockedConstruction feature is a powerful addition to the mocking framework that allows developers to mock the construction of new objects. This feature is particularly useful in scenarios where a method creates a new object during its execution, and the behavior of that object needs to be controlled during testing.

Why Use MockedConstruction?

In some cases, developers need to control the behavior of objects created within a method, which can be challenging to achieve with traditional mocking techniques. MockedConstruction addresses this problem by intercepting the constructor calls of a specific class and returning a pre-defined mock object instead. This feature is especially helpful when testing code that relies on third-party libraries, has complex object creation logic, or involves non-injectable dependencies.

ExampleConfiguration Class Overview

The ExampleConfiguration class contains two private methods, configureClients() and configureAuthentication(), which are called by the public method configure(). The configureClients() method creates a new instance of ClientConfiguration, while configureAuthentication() creates a new instance of AuthenticationManagementConfiguration. Both created objects are then used to call several configuration methods.

@AllArgsConstructor
public class ExampleConfiguration {

    public void configure() {
        configureAuthentication();
        configureClients();
    }

    private void configureClients() {
        ClientConfiguration clientConfiguration = new ClientConfiguration();

        clientConfiguration.configureClient();
        clientConfiguration.disableDefaultClients("excluded");
    }

    private void configureAuthentication() {
        AuthenticationManagementConfiguration authenticationManagementConfiguration = new AuthenticationManagementConfiguration();

        authenticationManagementConfiguration.configureBrowserFlow();
        authenticationManagementConfiguration.configureFirstBrokerLoginFlow();
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing the ExampleConfiguration Class

To test the ExampleConfiguration class using MockedConstruction, follow these steps:

  • Create a test class and annotate it with @ExtendWith(MockitoExtension.class) to enable Mockito's JUnit 5 integration.
@ExtendWith(MockitoExtension.class)
class ExampleConfigurationTest {
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • Write a test method to verify that the configure() method calls all the required configuration methods.
@Test
void configure_any_callsAllRequiredConfigurationMethods() {
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • Use the mockConstruction method to create MockedConstruction objects for both ClientConfiguration and AuthenticationManagementConfiguration classes.

By default, the MockedConstruction will return mocks for all method calls. You can use the withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS) option to specify that methods should call the real implementations.

MockedConstruction<ClientConfiguration> clientConfigMockedConstruction =
        mockConstruction(ClientConfiguration.class);
MockedConstruction<AuthenticationManagementConfiguration> authMgmtConfigMockedConstruction =
        mockConstruction(AuthenticationManagementConfiguration.class, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
Enter fullscreen mode Exit fullscreen mode
  • Invoke the method under test, exampleConfiguration.configure().
// when
exampleConfiguration.configure();
Enter fullscreen mode Exit fullscreen mode
  • Retrieve the mocked instances of ClientConfiguration and AuthenticationManagementConfiguration from their respective MockedConstruction objects, and use Mockito's verify() method to ensure the correct methods were called.
ClientConfiguration mockClientConfig = clientConfigMockedConstruction.constructed().get(0);
verify(mockClientConfig, times(1)).configureClient();
verify(mockClientConfig, times(1)).disableDefaultClients();

AuthenticationManagementConfiguration mockAuthMgmtConfig =
        authMgmtConfigMockedConstruction.constructed().get(0);
verify(mockAuthMgmtConfig, times(1)).configureBrowserFlow();
verify(mockAuthMgmtConfig, times(1)).configureFirstBrokerLoginFlow();
Enter fullscreen mode Exit fullscreen mode
  • Close the MockedConstruction objects to clean up the resources and ensure that the mocked constructors are deactivated after the test.
clientConfigMockedConstruction.close();
authMgmtConfigMockedConstruction.close();
Enter fullscreen mode Exit fullscreen mode

This test verifies that the configure() method of the ExampleConfiguration class calls all the required configuration methods on the instances of ClientConfiguration and AuthenticationManagementConfiguration. Here is the complete test:

@ExtendWith(MockitoExtension.class)
class ExampleConfigurationTest {

    private ExampleConfiguration exampleConfiguration = new ExampleConfiguration();

    @Test
    void configure_any_callsAllRequiredConfigurationMethods() {
        // given
        MockedConstruction<ClientConfiguration> clientConfigMockedConstruction =
                mockConstruction(ClientConfiguration.class);
        MockedConstruction<AuthenticationManagementConfiguration> authMgmtConfigMockedConstruction =
                mockConstruction(AuthenticationManagementConfiguration.class, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));

        // when
        exampleConfiguration.configure();

        // then
        ClientConfiguration mockClientConfig = clientConfigMockedConstruction.constructed().get(0);
        verify(mockClientConfig, times(1)).configureClient();
        verify(mockClientConfig, times(1)).disableDefaultClients();

        AuthenticationManagementConfiguration mockAuthMgmtConfig =
                authMgmtConfigMockedConstruction.constructed().get(0);
        verify(mockAuthMgmtConfig, times(1)).configureBrowserFlow();
        verify(mockAuthMgmtConfig, times(1)).configureFirstBrokerLoginFlow();

        clientConfigMockedConstruction.close();
        authMgmtConfigMockedConstruction.close();
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that clientConfigMockedConstruction and authMgmtConfigMockedConstruction are mocks and their behaviour can further be customized/mocked, for example when the return value of a method invocation is important. This can be achieved by the standard Mockito pattern using when(...).thenReturn(...).

Conclusion

Mockito's MockedConstruction feature provides an effective way to control the behavior of objects created within a method. By mastering this powerful feature, you can write more robust and efficient tests, especially when dealing with third-party libraries, complex object creation logic, or non-injectable dependencies.

Alternatives

Alternatives to this mocking technique include:

  • Partial mocking: Partial mocking allows you to mock specific methods of a class or an object while keeping the original behavior of other methods. This can be achieved using spy() in Mockito. However, partial mocking does not provide an easy way to control the behavior of objects created within methods.

  • Dependency Injection: Dependency injection is a technique used to pass dependencies to an object or a class, rather than having the object or class instantiate them internally. This allows for easier testing by injecting mock objects as dependencies. However, this approach may not be applicable in all scenarios, particularly when dealing with non-injectable dependencies (as the example at hand showed) or third-party libraries.

  • Setter Injection: In this approach, a class provides setter methods for its dependencies. During testing, mock objects can be provided to the class under test through these setter methods. However, this method may not be feasible when dealing with object creation inside methods.

  • Factory pattern: The factory pattern involves creating a separate factory class or method for object creation, making it easier to replace the object creation logic during testing. Although this can be a powerful technique, it may require significant refactoring of the code and may not be suitable for certain scenarios or third-party libraries.

However, three out of four alternatives that were named above include the refactoring of our actual code base in order to make a unit test more workable. That is an antipattern and discouraged.

Discussion (0)