【问题标题】:How to prevent Spring from injecting @Autowired references inside a mock?如何防止 Spring 在模拟中注入 @Autowired 引用?
【发布时间】:2015-09-29 14:22:21
【问题描述】:

我想使用 Spring + JUnit + Mockito 测试一个类,但我无法使其正常工作。

假设我的类引用了一个服务:

@Controller
public class MyController 
{

    @Autowired
    private MyService service;

    @PostConstruct
    public void init() {
        service.whatever();
    }

    public void doSomething() {
        service.create();
    }
}

而且这个服务引用了一个存储库:

@Service
public class MyService {

    @Autowired
    private MyRepository repository;

    public void whatever() {}

    public void create() {
        repository.save();
    }
}

在测试 MyController 类时,我希望模拟该服务。问题是:即使服务被模拟,Spring 也会尝试在模拟中注入存储库

这就是我所做的。测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { MyControllerTestConfiguration.class })
public class MyControllerTest {

    @Autowired
    private MyController myController;

    @Test
    public void testDoSomething() {
        myController.doSomething();
    }

}

配置类:

@Configuration
public class MyControllerTestConfiguration {

    @Bean
    public MyController myController() {
        return new MyController();
    }

    @Bean
    public MyService myService() {
        return Mockito.mock(MyService.class);
    }

}

我得到的错误是:org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [test.test.MyRepository] found for dependency

我尝试使用 Mockito 的 @InjectMocks 注释来初始化模拟,但这失败了,因为在模拟注入之前调用了 @PostConstruct 方法,生成了一个 NullPointerException

而且我不能简单地模拟存储库,因为在现实生活中这会让我模拟很多类......

谁能帮我解决这个问题?

【问题讨论】:

标签: java spring unit-testing junit mockito


【解决方案1】:

使用构造函数代替字段注入。这使测试变得容易得多。

@Service
public class MyService {

    private final MyRepository repository;

    @Autowired
    public MyService(MyRepository repository) {
        this.repository = repository;
    }

    public void whatever() {}

    public void create() {
        repository.save();
    }
}

-

@Controller
public class MyController {

    private final MyService service;

    @Autowired
    public MyController(MyService service) {
        this.service = service;
    }

    @PostConstruct
    public void init() {
        service.whatever();
    }

    public void doSomething() {
        service.create();
    }
}

这有几个优点:

  • 您的测试中不需要 Spring。这使您可以进行适当的单元测试。它还使测试速度非常快(从几秒到几毫秒)。
  • 您不能意外创建一个没有依赖关系的类的实例,这会导致NullPointerException
  • 正如@NamshubWriter 指出的那样:

    [依赖项的实例字段] 可以是最终的,因此 1) 它们不会被意外修改,并且 2) 读取该字段的任何线程都将读取相同的值。

丢弃@Configuration 类并编写如下测试:

@RunWith(MockitoJUnitRunner.class)
public class MyControllerTest {

    @Mock
    private MyRepository repository;

    @InjectMocks
    private MyService service;

    @Test
    public void testDoSomething() {
        MyController myController = new MyController(service);
        myController.doSomething();
    }

}

【讨论】:

  • 使用构造函数注入的一个好处是repositoryservice字段可以是final,所以1)它们不会被意外修改,2)读取该字段的任何线程都会读取相同的值。
  • 感谢您的回答,但这需要在实际应用程序中进行大量更改(并且会导致大量构造函数)。我很惊讶人们似乎同意不使用依赖注入:我认为依赖注入的目标是解决像我这样的情况(“在那种情况下,我想连接一个真正的服务,在另一种情况下,我想连接一个模拟")
  • 依赖注入的目标是让类不必知道如何获取或创建它们的依赖。当然,这也允许连接依赖项的不同实现,而无需更改依赖于它的类。但是,构造函数注入而不是在测试中使用 Spring 有几个优点。我相应地更新了我的答案。
【解决方案2】:

使用接口,特别是如果您使用某种 AOP(事务、安全等),即您将拥有接口 MyService 和类 MyServiceImpl

在配置中,您将拥有:

 @Bean
    public MyService myService() {
        return Mockito.mock(MyService.class);
    }

【讨论】:

  • 如果只有一个(非模拟)实现,我个人不喜欢创建接口,原因有两个。 1)它增加了维护成本(每次在类中添加或更改方法时,都需要更新接口),以及 2)它使阅读代码的实际作用变得更加困难,因为您需要了解配置看看选择了什么实现(这可以通过非常好的接口文档来减轻偏见)。一个你需要两个实现,提取一个实现有足够的好处来覆盖这些成本。
【解决方案3】:

你应该把@InjectMocks注解放在你的控制器里,把@Mock放在你的服务里,看:

@Autowired
@InjectMocks
private MyController myController;
@Autowired
@Mock
private MyService myService;

@Before
public void setup() {
    MockitoAnnotations.initMocks(this);
}

@Test
public void testDoSomething() {
    myController.doSomething();
}

【讨论】:

  • 谢谢,但由于 Mockito 在 MyController 实例化后注入了它的模拟,@PostConstruct 方法会抛出一个 NullPointerException
猜你喜欢
  • 1970-01-01
  • 2022-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-11
  • 1970-01-01
  • 2016-08-31
  • 2017-12-30
相关资源
最近更新 更多