50

我对 Mockito 很陌生,清理起来有些麻烦。

我曾经使用 JMock2 进行单元测试。据我所知,JMock2 将期望和其他模拟信息保留在将为每个测试方法重建的上下文中。因此,每种测试方法都不会受到其他方法的干扰。

在使用 JMock2 时,我对 spring 测试采用了相同的策略,我发现我在帖子中使用的策略存在潜在问题:为每种测试方法重建应用程序上下文,因此减慢了整个测试过程。

我注意到很多文章都推荐在春季测试中使用 Mockito,我想尝试一下。在我在测试用例中编写两个测试方法之前,它运行良好。每个测试方法单独运行时通过,如果它们一起运行,则其中一个失败。我推测这是因为模拟信息被保留在模拟本身中(因为我在 JMock 中没有看到任何类似的上下文对象)并且模拟(和应用程序上下文)在两种测试方法中都是共享的。

我通过在 @Before 方法中添加 reset() 来解决它。我的问题是处理这种情况的最佳做法是什么(reset() 的 javadoc 说如果你需要 reset(),代码是有味道的)?任何想法都值得赞赏,在此先感谢。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
    "file:src/main/webapp/WEB-INF/booking-servlet.xml",
    "classpath:test-booking-servlet.xml" })
@WebAppConfiguration
public class PlaceOrderControllerIntegrationTests implements IntegrationTests {

@Autowired
private WebApplicationContext wac;

private MockMvc mockMvc;

@Autowired
private PlaceOrderService placeOrderService;

@Before
public void setup() {
    this.mockMvc = webAppContextSetup(this.wac).build();

    reset(placeOrderService);// reset mock
}

@Test
public void fowardsToFoodSelectionViewAfterPendingOrderIsPlaced()
        throws Exception {

    final Address deliveryAddress = new AddressFixture().build();
    final String deliveryTime = twoHoursLater();
    final PendingOrder pendingOrder = new PendingOrderFixture()
            .with(deliveryAddress).at(with(deliveryTime)).build();

    when(placeOrderService.placeOrder(deliveryAddress, with(deliveryTime)))
            .thenReturn(pendingOrder);

    mockMvc.perform(...);

}

@Test
public void returnsToPlaceOrderViewWhenFailsToPlaceOrder() throws Exception {

    final Address deliveryAddress = new AddressFixture().build();
    final String deliveryTime = twoHoursLater();
    final PendingOrder pendingOrder = new PendingOrderFixture()
            .with(deliveryAddress).at(with(deliveryTime)).build();

    NoAvailableRestaurantException noAvailableRestaurantException = new NoAvailableRestaurantException(
            deliveryAddress, with(deliveryTime));
    when(placeOrderService.placeOrder(deliveryAddress, with(deliveryTime)))
            .thenThrow(noAvailableRestaurantException);

            mockMvc.perform(...);

}
4

6 回答 6

62
  1. 关于放置reset后的测试方法

    我认为最好在测试方法之后重置模拟,因为这意味着在测试期间确实发生了一些需要清理的事情。

    如果重置是在测试方法之前完成的,我会感到不确定,测试之前到底发生了什么应该重置的?那么非模拟对象呢?有没有理由(也许有)?如果代码中没有提到它是有原因的(例如方法名称)?等等。

  2. 不喜欢基于 Spring 的测试

    1. 背景

      使用 Spring 就像放弃对一个类进行单元测试;使用 Spring,您对测试的控制较少:隔离实例化生命周期,在单元测试中引用一些看起来的属性。然而,在许多情况下,Spring 提供的库和框架不是那么“透明”,为了测试你更好地测试整个东西的实际行为,比如 Spring MVC、Spring Batch 等。

      而制作这些测试要麻烦得多,因为在许多情况下,它要求开发人员制作集成测试来认真测试生产代码的行为。由于许多开发人员不了解您的代码如何在 Spring 中存在的所有细节,这可能会导致尝试使用单元测试来测试一个类时出现许多意外。

      但麻烦仍在继续,测试应该快速且小,以便向开发人员提供快速反馈(IDE 插件如Infinitest非常适合),但使用 Spring 的测试本质上更慢且更消耗内存。这往往会减少运行它们的频率,甚至完全避免在本地工作站上运行它们……以后在 CI 服务器上发现它们失败了。

    2. Mockito 和 Spring 的生命周期

      因此,当为子系统制作集成测试时,您最终会得到许多对象,显然还有协作者,这些对象可能会被嘲笑。生命周期由 Spring Runner 控制,但 Mockito 模拟不是。所以你必须自己管理模拟生命周期。

      再次关于在 Spring Batch 项目期间的生命周期,我们遇到了一些关于非模拟的残余影响的问题,所以我们有两个选择,每个测试类只使用一个测试方法,或者使用脏上下文技巧:@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)。这会导致测试速度变慢、内存消耗增加,但这是我们拥有的最佳选择。有了这个技巧,您就不必重置 Mockito 模拟了。

  3. 黑暗中可能的光明

    我不太了解这个项目,但是springockito可以为您提供一些关于生命周期的信息。注释子项目似乎更好:它似乎让 Spring 管理 Spring 容器中 bean 的生命周期,并让测试控制如何使用模拟。我仍然没有使用这个工具的经验,所以可能会有惊喜。

作为免责声明,我非常喜欢 Spring,它提供了许多卓越的工具来简化其他框架的使用,它可以提高生产力,它有助于设计,但就像人类发明的每一个工具一样,总有一个粗糙的边缘(如果不是更多的话...... )。

附带说明一下,看到这个问题恰好发生在JUnit上下文中很有趣,因为 JUnit 为每个测试方法实例化了测试类。如果测试基于TestNG,那么方法可能会有所不同,因为 TestNG 只创建一个测试类的实例,无论使用 Spring,都必须使用休息模拟字段。


老答案:

我不是在 spring 上下文中使用 Mockito 模拟的忠实粉丝。但是您是否正在寻找类似的东西:

@After public void reset_mocks() {
    Mockito.reset(placeOrderService);
}
于 2013-08-12T09:58:26.520 回答
21

Spring Boot 有@MockBean注释,您可以使用它来模拟您的服务。您不再需要手动重置模拟。只需替换@Autowired@MockBean

@MockBean
private PlaceOrderService placeOrderService;
于 2017-09-11T18:47:22.460 回答
6

而不是注入placeOrderService对象,您可能应该让 Mockito@Mock在每次测试之前将其初始化为 a,通过以下方式:

@Mock private PlaceOrderService placeOrderService;

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

正如此处 Javadoc 中所推荐的那样:http: //docs.mockito.googlecode.com/hg/latest/org/mockito/MockitoAnnotations.html

您甚至可以将该方法放在超类中,并为每个使用对象@Before的测试用例类扩展它。@Mock

于 2013-08-10T20:33:47.880 回答
5

Spring based test is hard to make fast and independed (as @Brice wrote). Here is a litle utility method for reset all mocks (you have to call it manually in every @Before method):

import org.mockito.Mockito;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;


public class MyTest {
    public void resetAll(ApplicationContext applicationContext) throws Exception {
        for (String name : applicationContext.getBeanDefinitionNames()) {
            Object bean = applicationContext.getBean(name);
            if (AopUtils.isAopProxy(bean) && bean instanceof Advised) {
                bean = ((Advised)bean).getTargetSource().getTarget();
            }
            if (Mockito.mockingDetails(bean).isMock()) {
                Mockito.reset(bean);
            }
        }
    }
}

As you see there is an iteration for all beans, check whether bean is mock or not, and reset the mock. I pay especially attention on call AopUtils.isAopProxy and ((Advised)bean).getTargetSource().getTarget(). If you bean contains an @Transactional annotation the mock of this bean always wrapped by spring into proxy object, so to reset or verify this mock you should unwrap it first. Otherwise you will get a UnfinishedVerificationException which can arise in different tests from time to time.

In my case AopUtils.isAopProxy is enough. But there are also AopUtils.isCglibProxy and AopUtils.isJdkDynamicProxy if you get troubles with proxying.

mockito is 1.10.19 spring-test is 3.2.2.RELEASE

于 2016-03-10T05:19:46.393 回答
3

在 spring 上下文中重置模拟的另一种方法。

https://github.com/Eedanna/mockito/issues/119#issuecomment-166823815

假设您使用的是 Spring,您可以通过获取 ApplicationContext 然后执行以下操作轻松地自己实现这一点:

public static void resetMocks(ApplicationContext context) {
    for ( String name : context.getBeanDefinitionNames() ) {
        Object bean = context.getBean( name );
        if (new MockUtil().isMock( bean )) {
            Mockito.reset( bean );
        }
    }
}

https://github.com/Eedanna/mockito/issues/119

将上述代码与 @AfterAll 耦合应该是清理/重置模拟的好方法。

于 2021-04-06T10:26:53.897 回答
1

您确实可以使用@MockBean如前所述)。它确实会在该上下文中的每个测试之后重置模拟,它也可能为不使用@MockBean/的相同精确组合的其他测试类重新构建整个弹簧上下文@SpyBean,这可能会导致构建测试阶段缓慢,因为许多上下文需要启动!

如果您使用的是 spring boot 2.2+,则可以使用@MockInBean作为@MockBean. 它将重置您的模拟并保持您的 Spring 上下文清洁(并快速测试)。

@SpringBootTest
public class MyServiceTest {

    @MockInBean(MyService.class)
    private ServiceToMock serviceToMock;

    @Autowired
    private MyService myService;

    @Test
    public void test() {
        Mockito.when(serviceToMock.returnSomething()).thenReturn(new Object());
        myService.doSomething();
    }
}

免责声明:我为此目的创建了这个库:清理模拟并避免在测试中重新创建 Spring 上下文常量。

于 2021-03-18T00:06:08.157 回答