我曾经使用JMock2进行单元测试。据我所知,JMock2在一个上下文中保留了期望值和其他模拟信息,该上下文将为每个测试方法重新构建。因此,每种测试方法都不受其他方法的干扰。
在使用JMock2时,我对春季测试采用了相同的策略,我发现我在帖子中使用的策略存在潜在问题:为每个测试方法重建应用程序上下文,因此会减慢整个测试过程。
我注意到许多文章建议在春季测试中使用Mockito,我想试一试。它运行良好,直到我在一个测试用例中编写了两个测试方法。每个测试方法在单独运行时都通过了,其中一个方法在一起运行时失败了。我推测这是因为mock信息被保存在mock本身中(因为我在JMock中没有看到任何类似的上下文对象),并且mock(和应用程序上下文)在两个测试方法中都是共享的。
我通过在@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(...);
}
- 关于在测试方法后重置
我认为重新设置mock应该在测试方法之后更好地完成,因为这意味着测试过程中确实发生了一些需要清理的事情。
如果重置是在测试方法之前完成的,我会感到不确定,在应该重置的测试之前到底发生了什么?非mocks对象呢?这是有原因的吗?如果代码中没有提到它是有原因的(例如方法名称)?等等。
-
不喜欢基于Spring的测试
-
背景
使用Spring就像放弃对类进行单元测试;使用Spring,您对测试的控制更少:隔离、实例化和生命周期,引用单元测试中的一些外观属性。然而,在许多情况下,Spring提供的库和框架并不是透明";,为了测试,你可以更好地测试整个东西的实际行为,比如Spring MVC、Spring Batch等。
制作这些测试要麻烦得多,因为在许多情况下,它迫使开发人员制作集成测试来认真测试生产代码的行为。由于许多开发人员并不了解您的代码在Spring中的每一个细节,所以尝试使用单元测试来测试类可能会带来很多惊喜。
但问题仍然存在,测试应该快速而小,以便向开发人员提供快速反馈(像Infinitest这样的IDE插件非常适合),但使用Spring的测试本质上更慢,更消耗内存。这往往会减少它们的运行频率,甚至完全避免它们在本地工作站上运行。。。以便稍后在CI服务器上发现它们失败。
-
Mockito和Spring 的生命周期
因此,当为一个子系统制定集成测试时,您最终会得到许多对象,显然还有合作者,这些对象可能会被嘲笑。生命周期由Spring Runner控制,但Mockito mock不是。所以你必须自己管理mocks的生命周期。
关于使用Spring Batch的项目的生命周期,我们在非mock上遇到了一些剩余影响的问题,所以我们有两个选择,每个测试类只做一个测试方法,或者使用肮脏的上下文技巧:
@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)
。这导致测试速度变慢,内存消耗增加,但这是我们的最佳选择。有了这个技巧,你就不必重置Mockito mocks了。 -
黑暗中可能有亮光
我对这个项目还不够了解,但springockito可以为您提供一些关于生命周期的糖。注释子项目似乎更好:它似乎让Spring管理Spring容器中bean的生命周期,并让测试控制mock的使用方式。尽管如此,我对这个工具没有任何经验,所以可能会有惊喜。
作为免责声明,我非常喜欢Spring,它提供了许多出色的工具来简化其他框架的使用,它可以提高生产力,有助于设计,但就像人类发明的每一种工具一样,总有一个粗糙的边缘(如果不是更多的话…)。
顺便说一句,当JUnit为每个测试方法实例化测试类时,在JUnit上下文中看到这个问题是很有趣的。如果测试基于TestNG,那么方法可能会有所不同,因为TestNG只创建测试类的一个实例,无论使用Spring,静态模拟字段都是强制性的。
老答案:
我不太喜欢在春季使用Mockito mocks。但你会找这样的东西吗:
@After public void reset_mocks() {
Mockito.reset(placeOrderService);
}
Spring Boot有@MockBean
注释,您可以使用它来模拟您的服务。您不再需要手动重置mock。只需将@Autowired
替换为@MockBean
:
@MockBean
private PlaceOrderService placeOrderService;
与其注入placeOrderService
对象,您可能只需要让Mockito在每次测试前通过以下方式将其初始化为@Mock
:
@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
对象的每个测试用例类扩展它。
基于Spring的测试很难快速且独立(正如@Brice所写)。这里有一个用于重置所有mock的litle实用程序方法(您必须在每个@Before
方法中手动调用它):
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);
}
}
}
}
正如您所看到的,所有bean都有一个迭代,检查bean是否为mock,并重置mock。我特别关注呼叫AopUtils.isAopProxy
和((Advised)bean).getTargetSource().getTarget()
。如果您的bean包含一个@Transactional
注释,那么这个bean的mock总是被spring包装到代理对象中,所以要重置或验证这个mock,您应该首先打开它。否则,你会得到一个UnfinishedVerificationException
,它可能会在不同的测试中不时出现。
就我而言,AopUtils.isAopProxy
就足够了。但如果代理遇到问题,也有AopUtils.isCglibProxy
和AopUtils.isJdkDynamicProxy
。
mockito是1.10.19
spring测试是3.2.2.RELEASE
在spring上下文中重置mock的另一种方法。
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耦合应该是清理/重置mock的好方法。
您确实可以使用@MockBean
(如前所述)。它确实会在该上下文中的每次测试后重置mock,但它也可能为不使用@MockBean
/@SpyBean
相同精确组合的其他测试类重新构建整个spring上下文,这可能会导致构建测试阶段缓慢,因为许多上下文需要启动!
如果您使用的是spring-boot 2.2+,则可以使用@MockInBean作为@MockBean
的替代方案。它将重置mock并保持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();
}
}
免责声明:我创建这个库的目的是:清理mock并避免在测试中重新创建Spring上下文常量