Spring has some cool testing features, and Spring Boot doesn’t just pack JUnit into its packages, but also Mockito, so we can use mocking to our hearts content. Mocking in Spring is just built-in.
We just need to be careful in our integration tests. We need to be wary of…
The Bean Life-cycle
Remember the whole dependency injection power of Spring? So far, we’ve used the phrase “when the application loads”, to say when beans are injected. And that is mostly true (unless you programmatically refresh the beans, more on that later).
“When the application loads”, the application context loads, and with it the whole set of Configurations, which include beans. For example:
@Configuration
public class MockPublisherConfiguration {
@Bean
public Publisher mockPublisher() {
Publisher mockPublisher = mock(Publisher.class);
return mockPublisher;
}
}
Code language: PHP (php)
So far, so good. We’re injecting a mock. Now let’s look at our completely contrived test class that uses that bean.
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = { MockPublisherConfiguration.class })
public class MockPublisherTests {
@Autowired Publisher mockPublisher;
@Test
public void firstTest() {
mockPublisher.publish();
Mockito.verify(mockPublisher).publish();
}
@Test
public void secondTest() {
verify(mockPublisher, never()).publish();
}
}
Code language: JavaScript (javascript)
Our tests check if the publish method was called. When we run it, the firstTest passes, and the secondTest fails. But why?
Our mockPublisher instance, used in both integration tests, is injected only once, when the test class instance is created. That’s the default injection mode in Spring, called “singleton” scope.
Using singletons is great in many use cases. However, we don’t want this behavior for mocking. We want mocks to be created every time from scratch, to lower the dependency between tests. In unit tests, we have full control on how to create any instance, so just recreating them in the setup or the test method is enough. With Spring integration tests it’s different.
So what can we do?
Using a prototype
When using “singleton” scope, the factory method is called once, and the instance is cached. Using “prototype” scope tells Spring that every time we need an instance, it should create a new instance, like we want from our mock.
Alas, in our integration tests, we have a single creation (or injection) for our mock. The factory method in the MockPublisherConfiguration is called only once.
But that’s because we’re using @Autowired. When the test class loads, Spring sees the @Autowired member and injects it, and that happens a only once. What if we get the bean programmatically, rather than through @Autowired?
Let’s change our test class a bit:
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = { MockPublisherConfiguration.class })
public class MockPublisherTests {
@Autowired ApplicationContext context;
Publisher mockPublisher;
@Before
public void setup() {
mockPublisher = context.getBean(Publisher.class);
}
@Test
public void firstTest() {
mockPublisher.publish();
Mockito.verify(mockPublisher).publish();
}
@Test
public void secondTest() {
verify(mockPublisher, never()).publish();
}
}
Code language: JavaScript (javascript)
As you can see, the mockPublisher is no longer @Autowired. We’re injecting the Spring ApplicationContext, and use it before every test. The getBean method fetches the mockPublisher instance for every test. This alone doesn’t do the trick, though. Remember, once the factory method is called, the instance is cached. Now, let’s change the Configuration class:
@Configuration
public class MockPublisherConfiguration {
@Bean
@Scope("prototype")
public Publisher mockPublisher() {
Publisher mockPublisher = mock(Publisher.class);
return mockPublisher;
}
}
Code language: PHP (php)
We’ve added the “prototype” scope to our bean and presto – Both integration tests pass!
Now we get a different instance for every test. The second one’s publish method indeed does not get called.
It works! We’ve got a different mock for each test.
Here’s the bad news: Most people don’t even understand how Spring works, so getting hot and heavy with the ApplicationContext may be too much for them. Not you, of course, but other people.
So for them, next time we’ll look at other options for mocking in Spring integration tests.
Check out the following workshops where I talk about testing in Spring:
0 Comments