简体   繁体   中英

Injecting Spring Boot component with Mockito mocks

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?


Update

Please see my GitHub repo for reproducing this exact issue.

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.

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;

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);
....
  }
}

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;
        }
    }
}

If you modify your StartupListenerTest to just focus on the StartupListener class

ie 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();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM