Integration Testing with Spring: A Custom Configuration

Gil Zilberfeld explains another part of Spring support for integration tests and configuration classes.
Standard
This is a short series of how to use Spring in integration testing and unit testing.
ConfigurationsMocking

Testing a REST API
A custom configurationConfiguration logic

Here’s the situation: We have a couple of configuration files we use for integration tests. Each of them is a different set of real and mock objects. Some of the objects have behaviors set on them (using Mockito.when) in the configuration classes.

Now, one of the integration tests need one of the mock beans in a slightly different setup. Since Spring initializes the mocks once, we have a problem: Either the new integration tests use the mock object as-is, or we call Mockito.reset, and then all the other integration tests suffer, since we don’t want to rely on the order of running.

But let’s step back for a minute. How did we get here?

This application is heavily Spring-based. Everything is injected and auto-wired, every small object is a @Bean, and so integration tests rely on configuration classes that contain all the beans needed to complete a flow.

Now, here’s the issue: Everything works fine until some integration tests need a custom configuration that is slightly modified. Such a configuration is expensive to build, and of course, managing multiple configuration sets is very cumbersome. How do we do that?

This is not an ideal situation to begin with, so there are no optimal ways to solve that, but let’s go through the options.

The first solution is to add another configuration class. But when the next @Bean is introduced, we’ll need to add it across all existing configurations. It won’t be long until we are in configuration hell. This process needs to be managed, and while it is the simpler solution, it is not sustainable.

The second solution, offered by Spring, is using the @Import annotation. Much like include files, we put the common beans in the imported configuration class, and the custom configuration classes use @Import to include the common file.

If you’ve worked with any type of include files before, you know what a pain it is to manage them. Also in big projects, we need to make sure everyone that edits configurations and adds tests, need to know the guidelines, which @Bean goes where. And then we create hierarchies of imported configurations. We’re back at configuration hell.

Even then, there are still cases where this doesn’t solve the problem completely. We still need to support adding a special bean, right there in the middle of the heirarchy, and to do that we need to shake the entire tree.

The third option is a modified custom version of configuration. We can use Java’s inheritance to create a special configuration that extends the “common” configuration class. Then we override just the bean we want.

For example, here’s a configuration class, containing a mock for tests:

Most of our integration tests use this configuration, but we want a special case where we want another mock behavior. We can do the following:

With that configuration available, our special integration tests can now use the OverridingMockConfiguration. This is easier than setting up a new configuration with all other beans, and less hassle than the other options we went through in terms of management.

However, this is not hassle-free: We need to keep this “special” separate from the “regular” configurations. We don’t want other people and tests using this configuration. Also, this configuration class is susceptible to change in its base class.

Like I said, there’s no good solution. Each has a maintenance and risk cost that comes with it.

What can reduce these costs? A better architecture. If there are less beans to inject, configurations become smaller, and so are their variations. Minimize those and the configuration costs and types go down.

Leave a Reply

Your email address will not be published. Required fields are marked *