Reputation: 605
Here is my GitHub repo for reproducing the exact issue.
Not sure if this is a Spring Boot question or a Mockito question.
I have the following Spring Boot @Component
class:
@Component
class StartupListener implements ApplicationListener<ContextRefreshedEvent>, KernelConstants {
@Autowired
private Fizz fizz;
@Autowired
private Buzz buzz;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// Do stuff involving 'fizz' and 'buzz'
}
}
So StartupListener
has no constructor and is intentionally a Spring @Component
that gets its properties injected via @Autowired
.
The @Configuration
class providing these dependencies is here, for good measure:
@Configuration
public class MyAppConfiguration {
@Bean
public Fizz fizz() {
return new Fizz("OF COURSE");
}
@Bean
public Buzz buzz() {
return new Buzz(1, true, Foo.Bar);
}
}
I am now trying to write a JUnit unit test for StartupListener
, and I have been using Mockito with great success. I would like to create a mock Fizz
and Buzz
instance and inject StartupListener
with them, but I'm not sure how:
public class StartupListenerTest {
private StartupListener startupListener;
@Mock
private Fizz fizz;
@Mock
price Buzz buzz;
@Test
public void on_startup_should_do_something() {
Mockito.when(fizz.calculateSomething()).thenReturn(43);
// Doesn't matter what I'm testing here, the point is I'd like 'fizz' and 'buzz' to be mockable mocks
// WITHOUT having to add setter methods to StartupListener and calling them from inside test code!
}
}
Any ideas as to how I can accomplish this?
Please see my GitHub repo for reproducing this exact issue.
Upvotes: 2
Views: 5313
Reputation: 2307
If you modify your StartupListenerTest to just focus on the StartupListener class
i.e. add the class to the SpringBootTest annotation
@SpringBootTest(classes= {StartupListener.class})
You will get a different error, but it's more focused on the class you're trying to test.
onApplicationEvent
method will fire before the test runs. This means you won't have initialized your mock with when(troubleshootingConfig.getMachine()).thenReturn(machine);
and so there's no Machine returned when getMachine() is called, hence the NPE.
The best approach to fix this really depends on what you're trying to achieve from the test. I would use an application-test.properties file to set up the TroubleShootingConfig rather than use an @MockBean. If all you're doing in your onApplicationEvent
is logging then you could use @SpyBean as suggested in another answer to this question. Here's how you could do it.
Add an application-test.properties
to resources folder so it's on the classpath:
troubleshooting.maxChildRestarts=4
troubleshooting.machine.id=machine-id
troubleshooting.machine.key=machine-key
Add @Configuration
to TroubleshootingConfig
@Configuration
@ConfigurationProperties(prefix = "troubleshooting")
public class TroubleshootingConfig {
private Machine machine;
private Integer maxChildRestarts;
... rest of the class
Change StartupListenerTest
to focus on the classes your testing and spy on the TroubleshootingConfig
. You also need to @EnableConfigurationProperties
@RunWith(SpringRunner.class)
@SpringBootTest(classes= {TroubleshootingConfig.class, StartupListener.class})
@EnableConfigurationProperties
public class StartupListenerTest {
@Autowired
private StartupListener startupListener;
@SpyBean
private TroubleshootingConfig troubleshootingConfig;
@MockBean
private Fizzbuzz fizzbuzz;
@Mock
private TroubleshootingConfig.Machine machine;
@Mock
private ContextRefreshedEvent event;
@Test
public void should_do_something() {
when(troubleshootingConfig.getMachine()).thenReturn(machine);
when(fizzbuzz.getFoobarId()).thenReturn(2L);
when(machine.getKey()).thenReturn("FLIM FLAM!");
// when
startupListener.onApplicationEvent(event);
// then
verify(machine).getKey();
}
}
Upvotes: 0
Reputation: 3882
Here is a simple example that just uses plain Spring.
package com.stackoverflow.q54318731;
import static org.junit.Assert.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
@SuppressWarnings("javadoc")
public class Answer {
/** The Constant SPRING_CLASS_RULE. */
@ClassRule
public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();
/** The spring method rule. */
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
static final AtomicInteger FIZZ_RESULT_HOLDER = new AtomicInteger(0);
static final int FIZZ_RESULT = 43;
static final AtomicInteger BUZZ_RESULT_HOLDER = new AtomicInteger(0);;
static final int BUZZ_RESULT = 42;
@Autowired
ConfigurableApplicationContext configurableApplicationContext;
@Test
public void test() throws InterruptedException {
this.configurableApplicationContext
.publishEvent(new ContextRefreshedEvent(this.configurableApplicationContext));
// wait for it
TimeUnit.MILLISECONDS.sleep(1);
assertEquals(FIZZ_RESULT, FIZZ_RESULT_HOLDER.get());
assertEquals(BUZZ_RESULT, BUZZ_RESULT_HOLDER.get());
}
@Configuration
@ComponentScan //so we can pick up the StartupListener
static class Config {
final Fizz fizz = Mockito.mock(Fizz.class);
final Buzz buzz = Mockito.mock(Buzz.class);
@Bean
Fizz fizz() {
Mockito.when(this.fizz.calculateSomething())
.thenReturn(FIZZ_RESULT);
return this.fizz;
}
@Bean
Buzz buzz() {
Mockito.when(this.buzz.calculateSomethingElse())
.thenReturn(BUZZ_RESULT);
return this.buzz;
}
}
@Component
static class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private Fizz fizz;
@Autowired
private Buzz buzz;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
FIZZ_RESULT_HOLDER.set(this.fizz.calculateSomething());
BUZZ_RESULT_HOLDER.set(this.buzz.calculateSomethingElse());
}
}
static class Fizz {
int calculateSomething() {
return 0;
}
}
static class Buzz {
int calculateSomethingElse() {
return 0;
}
}
}
Upvotes: 0
Reputation: 4207
you can do something likewise,
@RunWith(MockitoJUnitRunner.class)
public class StartupListenerTest {
@Mock
private Fizz fizz;
@Mock
price Buzz buzz;
@InjectMocks
private StartupListener startupListener;
@Test
public void on_startup_should_do_something() {
Mockito.when(fizz.calculateSomething()).thenReturn(43);
....
}
}
Upvotes: 3
Reputation: 4475
You can use @SpyBean
instead of @MockBean
, SpyBean
wraps the real bean but allows you to verify method invocation and mock individual methods without affecting any other method of the real bean.
@SpyBean
private Fizz fizz;
@SpyBean
price Buzz buzz;
Upvotes: 7
Reputation: 40008
You can use @MockBean
to mock beans in ApplicationContext
We can use the @MockBean to add mock objects to the Spring application context. The mock will replace any existing bean of the same type in the application context.
If no bean of the same type is defined, a new one will be added. This annotation is useful in integration tests where a particular bean – for example, an external service – needs to be mocked.
To use this annotation, we have to use SpringRunner to run the test:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class MockBeanAnnotationIntegrationTest {
@MockBean
private Fizz fizz;
}
And i will also suggest to use @SpringBootTest
The @SpringBootTest annotation tells Spring Boot to go and look for a main configuration class (one with @SpringBootApplication for instance), and use that to start a Spring application context.
Upvotes: 4