如何在单元测试中注入模拟的Spring @Autowired依赖项是最佳方式?

16
import org.springframework.beans.factory.annotation.Autowired;

class MyService {
  @Autowired private DependencyOne dependencyOne;
  @Autowired private DependencyTwo dependencyTwo;

  public void doSomething(){
    //Does something with dependencies
  }
}

测试这个类时,我基本上有四种方法来注入模拟依赖项:

  1. 在测试中使用Spring的ReflectionTestUtils注入依赖项
  2. 为MyService添加构造函数
  3. 为MyService添加setter方法
  4. 将依赖项可见性放宽到包保护,并直接设置字段

哪种方式最好,为什么?

--- 更新 ---

我想我应该更清楚一些 - 我只谈论“单元”风格的测试,而不是可以使用Spring上下文连线依赖项的Spring“集成”风格的测试。

3个回答

20

可以使用ReflectionTestUtils或者编写一个setter方法。两种方法都可以。添加构造函数可能会产生副作用(例如CGLIB无法创建子类),而仅为测试目的而放松可见性并不是一个好的方法。


2
通常来说我也会用 ReflectionTestUtils(反射测试工具类)。在我看来,这是 Spring 中为数不多的几个主要弱点之一,缺乏直接替换/模拟 bean 到上下文中的支持。 - pap
这是Spring中少数主要的弱点之一,即缺乏直接替换/模拟bean到上下文的支持 - 但在他的单元测试中没有Spring上下文。他只是使用new关键字创建了MyService类的实例。 - matt b
@matt b - 不要紧 - 这个问题是关于设置依赖项的可能性。 - Bozho
1
Setter方法很丑陋,因为它们只被测试使用,而RefelectionTestUtils也很丑陋,因为它无法跟上字段名称重构。哪个更不丑陋? - Daniel Alexiuc
2
setters :) 因为它们是设置依赖关系的常规方式。Spring 使用字段反射来实现,但也可以使用 setter 反射(bean 属性)。 - Bozho

3

Spring的ContextConfiguration可以帮助您实现这一点。

例如,在下面的测试上下文中,“Local”类是模拟类。NotificationService是我要测试的类。

我正在使用组件扫描将模拟类引入上下文,但您也可以轻松地使用<bean>声明。请注意使用use-default-filters="false"。

<context:component-scan base-package="com.foo.config" use-default-filters="false">
    <context:include-filter type="assignable" 
        expression="com.foo.LocalNotificationConfig"/>
</context:component-scan>

<context:component-scan base-package="com.foo.services.notification"
        use-default-filters="false">
    <context:include-filter type="assignable"
        expression="com.foo.services.notification.DelegatingTemplateService"/>
    <context:include-filter type="assignable"
        expression="com.foo.services.notification.NotificationService"/>
</context:component-scan>

<context:component-scan base-package="com.foo.domain"/>

DelegatingTemplateService是一个带有@Delegate注解的Groovy类。

class DelegatingTemplateService {
  @Delegate
  TemplateService delegate
}

在测试类中,我使用测试上下文并注入服务进行测试。在设置中,我设置了DelegatingTemplateService的委托:

@RunWith(classOf[SpringJUnit4ClassRunner])
@ContextConfiguration(Array("/spring-test-context.xml"))
class TestNotificationService extends JUnitSuite {
  @Autowired var notificationService: NotificationService = _
  @Autowired var templateService: DelegatingTemplateService = _

  @Before
  def setUp {
    templateService.delegate = /* Your dynamic mock here */
  }  

在服务中,@Autowired字段是私有的:
@Component("notificationService")
class NotificationServiceImpl extends NotificationService {
  @Autowired private var domainManager: DomainManager = _
  @Autowired private var templateService: TemplateService = _
  @Autowired private var notificationConfig: NotificationConfig = _

1
是的,您可以在测试和生产中交换实现。但是我考虑的是使用类似Mockito生成的运行时模拟对象,因此我的回答是这样的。 - Bozho
抓住 - 用Groovy @Delegate怎么样?请看我的更新答案。 - sourcedelica

2

2) 使用@Autowired构造函数注入(如果这是选项2;否则,创建一个选项5)

使用正确的构造函数可以创建处于有效状态的对象,这是一种更为正确的面向对象方法,失去cglib代理相对不重要,因为我们都遵循接口编码,是吗?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接