【问题标题】:Injecting Spring Boot component with Mockito mocks使用 Mockito 模拟注入 Spring Boot 组件
【发布时间】:2019-06-16 12:58:56
【问题描述】:

这是我的GitHub repo,用于重现确切的问题。

不确定这是 Spring Boot 问题还是 Mockito 问题。

我有以下 Spring Boot @Component 类:

@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'
    }
}

所以StartupListener 没有构造函数,并且故意是一个Spring @Component,它通过@Autowired 注入其属性。

提供这些依赖项的@Configuration 类在这里,很好衡量:

@Configuration
public class MyAppConfiguration {
    @Bean
    public Fizz fizz() {
        return new Fizz("OF COURSE");
    }

    @Bean
    public Buzz buzz() {
        return new Buzz(1, true, Foo.Bar);
    }
}

我现在正在尝试为 StartupListener 编写 JUnit 单元测试,并且我一直在使用 Mockito 并取得了巨大的成功。我想创建一个 mock FizzBuzz 实例并用它们注入 StartupListener,但我不确定如何:

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

有什么想法可以做到这一点吗?


更新

请查看我的GitHub repo 以重现此确切问题。

【问题讨论】:

  • 使用@MockBean
  • 您是否遇到任何错误?什么对你不起作用?只是,您可能缺少MockitoAnnotations.initMocks(this);@RunWith(SpringJUnit4ClassRunner.class)
  • 感谢@SergeiSirik (+1),但是当我尝试从我的配置类中抛出一个 NPE 时(请参阅上面的更新,其中包括完整的源代码)。有任何想法吗?再次感谢!
  • 另外@SergeiSirik 请查看我关于我将代码推送到的 GitHub 存储库的更新;它重现了我所看到的问题!提前感谢您的帮助!

标签: java unit-testing spring-boot junit mockito


【解决方案1】:

您可以使用@SpyBean而不是@MockBeanSpyBean 包装了真实 bean,但允许您验证方法调用和模拟单个方法,而不会影响真实 bean 的任何其他方法。

  @SpyBean
  private Fizz fizz;

  @SpyBean
  price Buzz buzz;

【讨论】:

  • 谢谢@stacker (+1) 你有理由相信@SpyBean 在我的情况下会比@MockBean 工作得更好吗?我愿意尝试,但不知道为什么一个会起作用,而另一个不起作用......
【解决方案2】:

您可以使用@MockBean 模拟ApplicationContext 中的bean

我们可以使用@MockBean 将模拟对象添加到Spring 应用程序上下文中。模拟将替换应用程序上下文中任何现有的相同类型的 bean。

如果没有定义相同类型的bean,将添加一个新的。此注解在需要模拟特定 bean(例如外部服务)的集成测试中很有用。

要使用这个注解,我们必须使用 SpringRunner 来运行测试:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class MockBeanAnnotationIntegrationTest {
 
@MockBean
private Fizz fizz;
 }

我也建议使用@SpringBootTest

@SpringBootTest 注解告诉 Spring Boot 去寻找一个主配置类(例如一个带有 @SpringBootApplication 的配置类),并使用它来启动一个 Spring 应用程序上下文。

【讨论】:

  • 感谢@Deadpool (+1),但是当我尝试从我的配置类中抛出 NPE 时(请参阅上面的更新,其中包括完整的源代码)。有任何想法吗?再次感谢!
  • 删除这个方法 setup 并模拟这些 ContextRefreshedEvent , Init@MockBean
  • 再次感谢 (+1) 请参阅我上面的修改,这些修改反映了您建议的更改。同样的错误仍然存​​在,从配置类中的authInfo() bean 创建方法抛出 NPE...
  • MyAppConfig 上删除此@Component 并在private StartupListener startupListener; 上添加@Autowired StartupListenerTest 类,并将SpringRunner 更改为SpringJUnit4ClassRunner
  • 从哪里注入这个TroubleshootingConfig 类属性? application.yml 在哪里? @hotmeatballsoup
【解决方案3】:

你也可以这样做,

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

【讨论】:

  • 对于 unit 测试,这在我看来是使用字段注入的最佳答案。此外,@RunWith(MockitoJUnitRunner.class) 是可选的,您可以将其删除。
【解决方案4】:

这是一个简单的例子,它只使用了普通的 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;
        }
    }
}

【讨论】:

    【解决方案5】:

    如果您将 StartupListenerTest 修改为只关注 StartupListener 类

    即将类添加到 SpringBootTest 注解中

    @SpringBootTest(classes= {StartupListener.class})
    

    您会得到一个不同的错误,但它更侧重于您尝试测试的课程。

    onApplicationEvent 方法将在测试运行之前触发。这意味着您不会使用 when(troubleshootingConfig.getMachine()).thenReturn(machine); 初始化您的模拟,因此在调用 getMachine() 时没有返回 Machine,因此是 NPE。

    解决此问题的最佳方法实际上取决于您要从测试中获得什么。我会使用 application-test.properties 文件来设置 TroubleShootingConfig 而不是使用 @MockBean。如果您在 onApplicationEvent 中所做的所有事情都是记录,那么您可以按照该问题的另一个答案中的建议使用 @SpyBean。这是你可以做到的。

    application-test.properties 添加到资源文件夹,使其位于类路径中:

    troubleshooting.maxChildRestarts=4
    troubleshooting.machine.id=machine-id
    troubleshooting.machine.key=machine-key
    

    @Configuration 添加到TroubleshootingConfig

    @Configuration
    @ConfigurationProperties(prefix = "troubleshooting")
    public class TroubleshootingConfig {
        private Machine machine;
        private Integer maxChildRestarts;
        ... rest of the class
    

    更改StartupListenerTest 以专注于您的测试和监视TroubleshootingConfig 的课程。你还需要@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();
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2021-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-16
      • 2022-01-25
      相关资源
      最近更新 更多