Building reusable simulation modules using spring boot

Time:2022-1-20

Building reusable mock modules with spring boot – reflecting

Building reusable simulation modules using spring boot

Isn’t it good to split the code base into loosely coupled modules, each with a set of special responsibilities?

This means that we can easily find each responsibility in the code base to add or modify code. It also means that the code base is easy to master, because we only need to load one module into the working memory of the brain at a time.

Moreover, since each module has its own API, this meansWe can create a reusable simulation for each module。 When writing an integration test, we just need to import a simulation module and call its API to start the simulation. We no longer need to know every detail of the class we simulate.

In this article, we will focus on creating such a module, discuss why it is better to simulate the whole module than a single bean, and then introduce a simple but effective method to simulate the complete module for simple test setup using spring boot.

 code example

A working code example on GitHub is attached to this article.

What is a module?

When I talk about “modules” in this article, I mean:

A module is a highly cohesive set of classes with dedicated APIs and a set of related responsibilities.

We can combine multiple modules into larger modules and finally form a complete application.

A module can use another module by calling its API.

You can also call them “components”, but in this article, I will stick to “modules”.

How to build modules?

When building applications, I recommend thinking ahead about how to modularize the code base. What are the natural boundaries in our code base?

Does our application need to communicate with external systems? This is a natural module boundary.We can build a module whose responsibility is to talk with external systems!

Do we determine the functional “boundary context” of the use cases that belong together? This is another good module boundary.We will build a module to implement the use cases in this functional part of the application!

Of course, there are more ways to split an application into modules, and it is often difficult to find the boundaries between them. They may even change over time! More importantly, we have a clear structure in our code base so that we can easily move concepts between modules!

In order to make the module obvious in our code base, I recommend using the following package structure:

  • Each module has its own package
  • Each module package has oneapiA sub package that contains all classes exposed to other modules
  • Each module package has an internal sub packageinternal, including:

    • All classes that implement the functions exposed by the API
    • A spring configuration class that provides beans to the spring application context required to implement the API
  • Like Russian dolls, each moduleinternalSub packages may contain packages with sub modules, each with its own API andinternalpackage
  • giveninternalClasses in a package can only be accessed by classes in the package.

This makes the code base very clear and easy to navigate. Read more about this code structure in my section on clear architectural boundaries, or some of the code in the sample code.

This is a good package structure, but what does it have to do with testing and simulation?

What’s the problem with simulating a single bean?

As I said at the beginning, we want to focus on simulating the entire module rather than a single bean. But what’s the problem with simulating a single bean first?

Let’s look at a very common way to create integration tests using spring boot.

Suppose we want to write an integration test for the rest controller, which should create a repository on GitHub and send e-mail to users.

The integration test may be as follows:

@WebMvcTest
class RepositoryControllerTestWithoutModuleMocks {


    @Autowired
    private MockMvc mockMvc;


    @MockBean
    private GitHubMutations gitHubMutations;


    @MockBean
    private GitHubQueries gitHubQueries;


    @MockBean
    private EmailNotificationService emailNotificationService;


  @Test
  void givenRepositoryDoesNotExist_thenRepositoryIsCreatedSuccessfully()
      throws Exception {
    String repositoryUrl = "https://github.com/reflectoring/reflectoring";
   
    given(gitHubQueries.repositoryExists(...)).willReturn(false);
    given(gitHubMutations.createRepository(...)).willReturn(repositoryUrl);
   
    mockMvc.perform(post("/github/repository")
      .param("token", "123")
      .param("repositoryName", "foo")
      .param("organizationName", "bar"))
      .andExpect(status().is(200));
   
    verify(emailNotificationService).sendEmail(...);
    verify(gitHubMutations).createRepository(...);
  }


}

This test actually looks neat, and I’ve seen (and written) a lot of similar tests. But as people say, details determine success or failure.

We use@WebMvcTestAnnotation to set the spring boot application context to test the spring MVC controller. The application context will contain all the beans needed to make the controller work, that’s all.

But our controller needs some extra beans in the application context to work, that isGitHubMutationsGitHubQueries, andEmailNotificationService。 So we passed@MockBeanAnnotations add simulations of these beans to the application context.

In the test method, we are in a pairgiven()The state of the simulation is defined in the statement, then the controller endpoint we want to test is invoked.verify()Some methods are used in the simulation.

So what’s wrong with this test? I thought of two main things:

First, setgiven()andverify()In this section, the test needs to know which methods on the mock bean the controller is calling.This low-level knowledge of implementation details makes the test easy to modify。 Every time the implementation details change, we must also update the test. This dilutes the value of testing and makes maintenance testing a chore rather than “sometimes routine”.

Second, the @ mockbean annotation will cause spring to create a new application context for each test (unless they have exactly the same fields). In a code base with multiple controllers, this will significantly increase test run time.

If we put a little effort into building the modular code base outlined in the previous section, we can address these two shortcomings by building reusable simulation modules.

Let’s see how to implement it by looking at a specific example.

Modular spring boot application

OK, let’s see how to use spring boots to implement reusable simulation modules.

This is the folder structure of the sample application. If you want to follow, you can find the code on GitHub:

├── github
|   ├── api
|   |  ├── <I> GitHubMutations
|   |  ├── <I> GitHubQueries
|   |  └── <C> GitHubRepository
|   └── internal
|      ├── <C> GitHubModuleConfiguration
|      └── <C> GitHubService
├── mail
|   ├── api
|   |  └── <I> EmailNotificationService
|   └── internal
|      ├── <C> EmailModuleConfiguration
|      ├── <C> EmailNotificationServiceImpl
|      └── <C> MailServer
├── rest
|   └── internal
|       └── <C> RepositoryController
└── <C> DemoApplication

The application has three modules:

  • githubThe module provides an interface to interact with the GitHub API,
  • mailThe module provides e-mail function,
  • restThe module provides a rest API to interact with applications.

Let’s look at each module in more detail.

GitHub module

githubThe module provides two interfaces<I>Tag) as part of its API:

  • GitHubMutations, provides some write operations to GitHub API,
  • GitHubQueries, which provides some read operations to the GitHub API.

This is what the interface looks like:

public interface GitHubMutations {


    String createRepository(String token, GitHubRepository repository);


}


public interface GitHubQueries {


    List<String> getOrganisations(String token);


    List<String> getRepositories(String token, String organisation);


    boolean repositoryExists(String token, String repositoryName, String organisation);


}

It also provides classesGitHubRepository, the signature used for these interfaces.

Inside,githubModule has classGitHubService, it implements two interfaces and classesGitHubModuleConfiguration, which is a spring configuration that contributes to the application contextGitHubServiceexample:

@Configuration
class GitHubModuleConfiguration {


    @Bean
    GitHubService gitHubService() {
        return new GitHubService();
    }


}

becauseGitHubServiceRealizedgithubThe entire API of the module, so this bean is enough to make the API of the module available to other modules in the same spring boot application.

Mail module

mailModules are built in a similar way. Its API consists of a single interfaceEmailNotificationServiceform:

public interface EmailNotificationService {


    void sendEmail(String to, String subject, String text);


}

The interface is controlled internallybeanEmailNotificationServiceImplrealization.

Please note that I ammailThe naming convention used in the module is the same as ingithubDifferent naming conventions are used in modules.githubThe module has a*ServiceeThe inner class at the end, andmailThe module has a*ServiceClass as part of its API. althoughgithubModules do not use ugly*ImplSuffix, butmailThe module uses.

I did this deliberately to make the code more realistic. Have you ever seen a code base (not written by yourself) use the same naming convention everywhere? I didn’t.

However, if you build modules like we did in this article, it doesn’t really matter. Because ugly*ImplClass is hidden behind the API of the module.

Inside,mailModule hasEmailModuleConfigurationClass, which provides API implementation for spring application context:

@Configuration
class EmailModuleConfiguration {


    @Bean
    EmailNotificationService emailNotificationService() {
        return new EmailNotificationServiceImpl();
    }


}

Rest module

restThe module consists of a single rest controller:

@RestController
class RepositoryController {


    private final GitHubMutations gitHubMutations;
    private final GitHubQueries gitHubQueries;
    private final EmailNotificationService emailNotificationService;


    // constructor omitted


    @PostMapping("/github/repository")
    ResponseEntity<Void> createGitHubRepository(@RequestParam("token") String token,
            @RequestParam("repositoryName") String repoName, @RequestParam("organizationName") String orgName) {


        if (gitHubQueries.repositoryExists(token, repoName, orgName)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
        String repoUrl = gitHubMutations.createRepository(token, new GitHubRepository(repoName, orgName));
        emailNotificationService.sendEmail("[email protected]", "Your new repository",
                "Here's your new repository: " + repoUrl);


        return ResponseEntity.ok().build();
    }


}

Controller callgithubModule API to create a GitHub repository, and thenmailModule API sends mail to let users know the new warehouse.

Analog GitHub module
Now let’s look at how to build a reusable simulation for the GitHub module. We created a@TestConfigurationClass, which provides all beans of the module API:

@TestConfiguration
public class GitHubModuleMock {


    private final GitHubService gitHubServiceMock = Mockito.mock(GitHubService.class);


    @Bean
    @Primary
    GitHubService gitHubServiceMock() {
        return gitHubServiceMock;
    }


    public void givenCreateRepositoryReturnsUrl(String url) {
        given(gitHubServiceMock.createRepository(any(), any())).willReturn(url);
    }


    public void givenRepositoryExists() {
        given(gitHubServiceMock.repositoryExists(anyString(), anyString(), anyString())).willReturn(true);
    }


    public void givenRepositoryDoesNotExist() {
        given(gitHubServiceMock.repositoryExists(anyString(), anyString(), anyString())).willReturn(false);
    }


    public void assertRepositoryCreated() {
        verify(gitHubServiceMock).createRepository(any(), any());
    }


    public void givenDefaultState(String defaultRepositoryUrl) {
        givenRepositoryDoesNotExist();
        givenCreateRepositoryReturnsUrl(defaultRepositoryUrl);
    }


    public void assertRepositoryNotCreated() {
        verify(gitHubServiceMock, never()).createRepository(any(), any());
    }


}

In addition to providing a simulatedGitHubServiceBean, we also added a bunch to this classgiven*()andassert*()method.

Givengiven*()Method allows us to set the simulation to the desired state, andverify*()Method allows us to check whether the interaction with the simulation occurs after running the test.

@PrimaryAnnotations ensure that simulation takes precedence if both simulated and real beans are loaded into the application context.

Simulated email module

We aremailThe module builds a very similar simulation configuration:

@TestConfiguration
public class EmailModuleMock {


    private final EmailNotificationService emailNotificationServiceMock = Mockito.mock(EmailNotificationService.class);


    @Bean
    @Primary
    EmailNotificationService emailNotificationServiceMock() {
        return emailNotificationServiceMock;
    }


    public void givenSendMailSucceeds() {
        // nothing to do, the mock will simply return
    }


    public void givenSendMailThrowsError() {
        doThrow(new RuntimeException("error when sending mail")).when(emailNotificationServiceMock)
                .sendEmail(anyString(), anyString(), anyString());
    }


    public void assertSentMailContains(String repositoryUrl) {
        verify(emailNotificationServiceMock).sendEmail(anyString(), anyString(), contains(repositoryUrl));
    }


    public void assertNoMailSent() {
        verify(emailNotificationServiceMock, never()).sendEmail(anyString(), anyString(), anyString());
    }


}

Use the simulation module in the test

Now, with the simulation modules, we can use them in the integration test of the controller:

@WebMvcTest
@Import({ GitHubModuleMock.class, EmailModuleMock.class })
class RepositoryControllerTest {


    @Autowired
    private MockMvc mockMvc;


    @Autowired
    private EmailModuleMock emailModuleMock;


    @Autowired
    private GitHubModuleMock gitHubModuleMock;


    @Test
    void givenRepositoryDoesNotExist_thenRepositoryIsCreatedSuccessfully() throws Exception {


        String repositoryUrl = "https://github.com/reflectoring/reflectoring.github.io";


        gitHubModuleMock.givenDefaultState(repositoryUrl);
        emailModuleMock.givenSendMailSucceeds();


        mockMvc.perform(post("/github/repository").param("token", "123").param("repositoryName", "foo")
                .param("organizationName", "bar")).andExpect(status().is(200));


        emailModuleMock.assertSentMailContains(repositoryUrl);
        gitHubModuleMock.assertRepositoryCreated();
    }


    @Test
    void givenRepositoryExists_thenReturnsBadRequest() throws Exception {


        String repositoryUrl = "https://github.com/reflectoring/reflectoring.github.io";


        gitHubModuleMock.givenDefaultState(repositoryUrl);
        gitHubModuleMock.givenRepositoryExists();
        emailModuleMock.givenSendMailSucceeds();


        mockMvc.perform(post("/github/repository").param("token", "123").param("repositoryName", "foo")
                .param("organizationName", "bar")).andExpect(status().is(400));


        emailModuleMock.assertNoMailSent();
        gitHubModuleMock.assertRepositoryNotCreated();
    }


}

We use@ImportAnnotations import the simulation into the application context.

Please note that,@WebMvcTestAnnotations also cause the actual module to be loaded into the application context. This is what we use in simulation@PrimaryAnnotate the reasons for simulation priority.

How to handle modules with abnormal behavior?

The module may attempt to connect to some external services during startup and behave abnormally. For example,mailThe module may create an SMTP connection pool at startup. This naturally fails when no SMTP server is available. This means that when we load the module in the integration test, the start of the spring context will fail.
To make the module perform better during testing, we can introduce a configuration attributemail.enabled。 Then we use@ConditionalOnPropertyAnnotate the configuration class of the module to tell spring if the property is set tofalse, do not load this configuration.
Now, during the test, only the simulation module is loaded.

Instead of simulating a specific method call in the test, we call the prepared method in the simulation modulegiven*()method. That meansTesting no longer requires the internal knowledge of the class called by the test object

After executing the code, we can use the preparedverify*()Method to verify whether a repository has been created or a message has been sent. Similarly, the specific underlying method calls are not known.

If we need another controllergithubormailModule, we can use the same simulation module in the test of the controller.

If we later decide to build another integration that uses a real version of some modules but a simulated version of other modules, we only need to use a few @ import annotations to build the application context we need.

This is the whole idea of the module: we can use the simulation of real module a and module B, and we still have a working application that can run tests

The simulation module is the center of our simulation behavior in this module. They can translate high-level simulation expectations such as “make sure you can create a repository” into low-level calls to API bean simulation.

conclusion

By consciously understanding what is part of the module API and what is not, we can build an appropriate modular code base with few unnecessary dependencies.

Since we know what is part of the API and what is not, we can build a dedicated simulation for each module’s API. We don’t care about the inside, we’re just simulating the API.

Simulation modules can provide APIs to simulate certain states and verify certain interactions. By using the API of the simulation module instead of simulating each individual method call, our integration tests become more flexible to adapt to changes.