每个单元测试后创建一个新的bean实例

17

我刚接触Spring框架,对于它在Spring上下文中使用的依赖注入功能有疑问。

这是我正在尝试为其编写集成测试的类:

public class UserService {

private Validator validator;
private UserRepository userRepository;
private Encryptor encryptor;
private MailService mailService;

...

public void registerUser(User user) {
    user.setPassword(encryptor.encrypt(user.getPassword()));

    Errors errors = new BindException(user, "user");
    validator.validate(user, errors);

    if (errors.getErrorCount() == 0) {
        userRepository.addUser(user);
        mailService.sendMail(user.getEmail());
    }
}

在我的测试中(使用 Mockito),我想要确保四个项都被调用,因此我创建了以下的测试:

public void testRegisterCallsValidateInValidator() {
    userService.registerUser(testUser);
    verify(userService.getValidator(), times(1)).validate(any(User.class), any(Errors.class));
}

所有测试都失败,提示我多次调用了该方法。我的猜测是,在所有测试开始时只创建一次UserService bean,但在每个测试结束后没有重新加载。

在我的测试配置中,我使用以下xml文件来决定要注入哪些bean:

<bean id="userService" class="be.kdg.coportio.services.UserService">
    <property name="validator" ref="validator"/>
    <property name="userRepository" ref="userRepository"/>
    <property name="encryptor" ref="encryptor"/>
    <property name="mailService" ref="mailService"/>
</bean>

有什么想法吗?


你有多个测试方法,还是只有你粘贴的那一个? - driangle
我有四个测试方法(其中一个已经复制粘贴)。我得到了三个失败的测试,说我分别调用了我试图测试的方法2、3和4次。 - Geoffrey De Vylder
5个回答

39

你正在重复使用你的上下文,为了使测试相互独立,你可能需要在每个测试之后刷新你的上下文以重新设置所有内容。

我假设你正在使用Junit 4.5 +。如果使用其他测试框架,应该类似。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"mycontext.xml"})
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class MyTestClass {
...
    // my tests    
...
}
如果待修复的方法只有几个,你可以在方法级别上放置@DirtiesContext。但是,如果你使用Spring,则最好在每个测试后执行此操作。无论如何,我认为你不应该在集成测试中使用模拟/间谍(mocks/spies):
- 在单元测试中,使用模拟对象(如果需要)并手动注入。这里你希望验证被测试bean的行为作为一个单元,所以你通过使用模拟将其与其他部分隔离开来。这也有一个优点,即JUnit使用测试类的不同实例对每个测试进行隔离,因此除非你使用静态或其他不友好的测试实践,否则一切都会正常工作。 - 在集成测试中,使用真实的bean并让Spring注入。这里的目标是验证bean之间/与环境(数据库、网络等)的交互是否良好。你不想在这里隔离bean,因此不应使用模拟对象。
更详细的解释请参见Spring测试文档

感谢提供代码示例。正如我在Eugen的回答中所评论的,我混淆了两种测试。这就是我将在我的实际集成测试中使用的内容 :) - Geoffrey De Vylder
非常感谢。这也可以应用于扩展AbstractTestNGSpringContextTests的TestNG测试类。 - Sebastian Saip
AFTER_EACH_TEST_METHOD,太棒了。这真的有助于为我的每个测试创建新的页面对象。 - Will
有时候使用BEFORE_EACH_TEST_METHOD更安全,因为bean可能具有其他测试类的状态。 - Jakub

13
为了清晰地区分单元测试和集成测试(跳过每个类别的争论)- 你可以通过两种方式测试服务:
  • 通过一个集成测试 - 启动整个 Spring 上下文并将服务作为单例 bean 进行测试。
  • 通过一个单元测试 - 你只需自己初始化服务,模拟需要模拟的内容,不需要使用 Spring。
我的建议是如果可能的话不要混合 Spring 和模拟 - 在单元测试中保留 Mockito(这是你看起来需要的)并使用集成测试来引导整个 Spring 上下文以测试其他事物 - 持久性问题、事务等。
你不需要 Spring 来模拟类的协作者并使用 Mockito 进行简单的交互测试。

2
我的建议是,如果可以的话,请不要混合使用Spring和mocks。如果您有setter或使用Spring的ReflectionTestUtils.setField()方法,则手动注入mocks。如果您的服务在每个测试中都会更改,则无需启动spring上下文。 - Sean Patrick Floyd
你的建议帮助我澄清了一些事情。我一直在错误地使用“集成测试”这个词,而实际上我想说的是交互测试。我最初选择不使用Spring来注入我的模拟对象,所以现在我会切换回那段代码,并且只在真正的集成测试中使用Spring。谢谢! - Geoffrey De Vylder

3
在你的@Before方法中,请确保重置你的模拟对象。
@Before
public void setup(){
    Mockito.reset(validator);
}

由于某种原因,其中一个测试现在已经修复,但其他测试仍然失败(与之前相同的消息)。我在设置方法中放置了4行代码,如下所示:Mockito.reset(userService.getValidator());也许它失败是因为我正在使用getter?我只在测试类中有一个UserService属性,而不是4个单独的对象。 - Geoffrey De Vylder

0
你可以在测试方法中调用setDirty(true)来重新加载Spring上下文。

0

我从未使用过Mockito,但Spring-Beans默认情况下是单例的 - 因此除非您在Spring容器上调用refresh(),否则它们不会被重新创建。

如果您无论如何都不需要它们成为单例,您可以将它们的作用域设置为prototype,这将在每次注入时创建新的bean实例...


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