@Autowired 依赖的@Mock 导致随机 junit 测试失败
@Mock of an @Autowired dependency causes random junit test failures
我的 spring-mvc 应用可以运行。耶!证明:
这是我的好设置:
我的重要部分 Buggy-servlet.xml
<import resource="classpath:bug-core.xml" />
<mvc:annotation-driven />
<context:component-scan base-package="buggy.bug" />
它导入的 bug-core.xml
文件的重要部分:
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />
<bean id="VersionInfoBean" class="buggy.bug.VersionInfo">
<property name="helloWorld" value="GAHHHHH!!!" />
</bean>
VersionInfo
class:
public class VersionInfo {
private String helloWorld;
public String getHelloWorld() {
return helloWorld;
}
public void setHelloWorld(String helloWorld) {
this.helloWorld = helloWorld;
}
}
最后,VersionInfoController
class:
@RestController
@RequestMapping("/versioninfo")
public class VersionInfoController {
@Autowired
private VersionInfo versionInfo;
@ResponseStatus(value = HttpStatus.OK)
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public VersionInfo getVersionInfo () {
return versionInfo;
}
}
一切都很好!
现在的问题:
我想进行单元测试。我觉得我做得很好。我的 VersionInfoControllerTest
class:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
@WebAppConfiguration
@TestExecutionListeners(listeners = {DependencyInjectionTestExecutionListener.class})
public class VersionInfoControllerTest {
// TODO: apparently I cannot @Mock the VersionInfo. Try uncommenting the below, run the test a few times and see.
// The link is for testng, but it's nearly the same for junit, and SHOULD work!
// https://lkrnac.net/blog/2014/01/mock-autowired-fields/
// @Mock
// private VersionInfo versionInfo;
@InjectMocks
private VersionInfoController versionInfoController;
private MockMvc mockMvc;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(versionInfoController).build();
}
@Test
public void getVersionInfo() throws Exception {
mockMvc.perform(get("/versioninfo")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
WebAppContext
class:
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"buggy.bug"})
public class WebAppContext extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
TestContext
class:
@Configuration
public class TestContext {
@Bean
public VersionInfo versionInfo() {
return Mockito.mock(VersionInfo.class);
}
}
I 运行 junit(通过 mvn clean install
或在 Eclipse junit 启动配置中)。一切都很好。
如果我取消注释 VersionInfoControllerTest
中指示的两行,测试可能会失败或通过(失败的情况更多)。当它失败时,它会以两种方式之一失败:
方式一:
java.lang.AssertionError: Status expected:<200> but was:<500>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:60)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:89)
at org.springframework.test.web.servlet.result.StatusResultMatchers.match(StatusResultMatchers.java:655)
at org.springframework.test.web.servlet.MockMvc.andExpect(MockMvc.java:171)
at buggy.bug.VersionInfoControllerTest.getVersionInfo(VersionInfoControllerTest.java:52)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:254)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
at org.junit.runners.ParentRunner.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access[=17=]0(ParentRunner.java:53)
at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:229)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
方式二更糟糕:
INFO: FrameworkServlet '': initialization completed in 1 ms
Jun 03, 2016 8:44:06 PM org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver handleHttpMessageNotWritable
WARNING: Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: Infinite recursion (WhosebugError) (through reference chain: org.mockito.internal.stubbing.InvocationContainerImpl["invocationForStubbing"]->org.mockito.internal.invocation.InvocationMatcher["invocation"]->org.mockito.internal.invocation.InvocationImpl["mock"]->buggy.bug.VersionInfo$$EnhancerByMockitoWithCGLIB$545457["callbacks"]->org.mockito.internal.creation.MethodInterceptorFilter["handler"]->org.mockito.internal.handler.InvocationNotifierHandler["invocationContainer"]->org.mockito.internal.stubbing.InvocationContainerImpl["invocationForStubbing"]->org.mockito.internal.invocation.InvocationMatcher["invocation"]->org.mockito.internal.invocation.InvocationImpl["mock"]->buggy.bug.VersionInfo$$EnhancerByMockitoWithCGLIB$545457["callbacks"]->org.mockito.internal.creation.MethodInterceptorFilter["handler"]->org.mockito.internal.handler.InvocationNotifierHandler["invocationContainer"]->org.mockito.internal.stubbing.InvocationContainerImpl["invocationForStubbing"]->...
因为它是 WhosebugError,重复直到内存不足。
我放了一个 github project that demonstrates the problem
有什么想法吗?我做错了什么?据我所知,我确实做了 SO 和博客、论坛、spring 文档上其他接受的答案,都说要模拟 @Autowired 字段。
这里发生了两件事。
- 由于您使用的是
MockMvcBuilders.standaloneSetup
,因此 不需要 加载 ApplicationContext
。
- Jackson 无法将您的
VersionInfo
对象转换为 JSON 如果它是由 Mockito 创建的模拟(至少不能使用尝试映射所有属性的默认映射规则,包括 Mockito 引入的属性)。
解决方法如下:
public class VersionInfo {
@JsonView(VersionInfo.class)
private String helloWorld;
public String getHelloWorld() {
return helloWorld;
}
public void setHelloWorld(String helloWorld) {
this.helloWorld = helloWorld;
}
}
@RestController
@RequestMapping("/versioninfo")
public class VersionInfoController {
@Autowired
private VersionInfo versionInfo;
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@JsonView(VersionInfo.class)
public VersionInfo getVersionInfo() {
return versionInfo;
}
}
public class VersionInfoControllerTest {
@Mock
private VersionInfo versionInfo;
@InjectMocks
private VersionInfoController versionInfoController;
private MockMvc mockMvc;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(versionInfoController).build();
}
@Test
public void getVersionInfo() throws Exception {
mockMvc.perform(get("/versioninfo").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
总结:
- 使用标准 JUnit 4 测试支持并删除与 Spring TestContext Framework 相关的所有内容,因为您没有使用
ApplicationContext
.
- 在
VersionInfo
中使用 @JsonView
将 JSON 序列化仅限于 VersionInfo
. 中的属性
- 在控制器的
getVersionInfo()
方法上使用 @JsonView
来指示 Spring 在调用 Jackson [=57] 时使用 view =]映射器。
请记住,在这种情况下使用 @JsonView
只是必要的,因为您正在使用 Mockito 从控制器方法中模拟 return 值。
此致,
Sam(Spring TestContext Framework 的作者)
我的 spring-mvc 应用可以运行。耶!证明:
这是我的好设置:
我的重要部分 Buggy-servlet.xml
<import resource="classpath:bug-core.xml" />
<mvc:annotation-driven />
<context:component-scan base-package="buggy.bug" />
它导入的 bug-core.xml
文件的重要部分:
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />
<bean id="VersionInfoBean" class="buggy.bug.VersionInfo">
<property name="helloWorld" value="GAHHHHH!!!" />
</bean>
VersionInfo
class:
public class VersionInfo {
private String helloWorld;
public String getHelloWorld() {
return helloWorld;
}
public void setHelloWorld(String helloWorld) {
this.helloWorld = helloWorld;
}
}
最后,VersionInfoController
class:
@RestController
@RequestMapping("/versioninfo")
public class VersionInfoController {
@Autowired
private VersionInfo versionInfo;
@ResponseStatus(value = HttpStatus.OK)
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public VersionInfo getVersionInfo () {
return versionInfo;
}
}
一切都很好!
现在的问题:
我想进行单元测试。我觉得我做得很好。我的 VersionInfoControllerTest
class:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
@WebAppConfiguration
@TestExecutionListeners(listeners = {DependencyInjectionTestExecutionListener.class})
public class VersionInfoControllerTest {
// TODO: apparently I cannot @Mock the VersionInfo. Try uncommenting the below, run the test a few times and see.
// The link is for testng, but it's nearly the same for junit, and SHOULD work!
// https://lkrnac.net/blog/2014/01/mock-autowired-fields/
// @Mock
// private VersionInfo versionInfo;
@InjectMocks
private VersionInfoController versionInfoController;
private MockMvc mockMvc;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(versionInfoController).build();
}
@Test
public void getVersionInfo() throws Exception {
mockMvc.perform(get("/versioninfo")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
WebAppContext
class:
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"buggy.bug"})
public class WebAppContext extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
TestContext
class:
@Configuration
public class TestContext {
@Bean
public VersionInfo versionInfo() {
return Mockito.mock(VersionInfo.class);
}
}
I 运行 junit(通过 mvn clean install
或在 Eclipse junit 启动配置中)。一切都很好。
如果我取消注释 VersionInfoControllerTest
中指示的两行,测试可能会失败或通过(失败的情况更多)。当它失败时,它会以两种方式之一失败:
方式一:
java.lang.AssertionError: Status expected:<200> but was:<500>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:60)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:89)
at org.springframework.test.web.servlet.result.StatusResultMatchers.match(StatusResultMatchers.java:655)
at org.springframework.test.web.servlet.MockMvc.andExpect(MockMvc.java:171)
at buggy.bug.VersionInfoControllerTest.getVersionInfo(VersionInfoControllerTest.java:52)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:254)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
at org.junit.runners.ParentRunner.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access[=17=]0(ParentRunner.java:53)
at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:229)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
方式二更糟糕:
INFO: FrameworkServlet '': initialization completed in 1 ms
Jun 03, 2016 8:44:06 PM org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver handleHttpMessageNotWritable
WARNING: Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: Infinite recursion (WhosebugError) (through reference chain: org.mockito.internal.stubbing.InvocationContainerImpl["invocationForStubbing"]->org.mockito.internal.invocation.InvocationMatcher["invocation"]->org.mockito.internal.invocation.InvocationImpl["mock"]->buggy.bug.VersionInfo$$EnhancerByMockitoWithCGLIB$545457["callbacks"]->org.mockito.internal.creation.MethodInterceptorFilter["handler"]->org.mockito.internal.handler.InvocationNotifierHandler["invocationContainer"]->org.mockito.internal.stubbing.InvocationContainerImpl["invocationForStubbing"]->org.mockito.internal.invocation.InvocationMatcher["invocation"]->org.mockito.internal.invocation.InvocationImpl["mock"]->buggy.bug.VersionInfo$$EnhancerByMockitoWithCGLIB$545457["callbacks"]->org.mockito.internal.creation.MethodInterceptorFilter["handler"]->org.mockito.internal.handler.InvocationNotifierHandler["invocationContainer"]->org.mockito.internal.stubbing.InvocationContainerImpl["invocationForStubbing"]->...
因为它是 WhosebugError,重复直到内存不足。
我放了一个 github project that demonstrates the problem
有什么想法吗?我做错了什么?据我所知,我确实做了 SO 和博客、论坛、spring 文档上其他接受的答案,都说要模拟 @Autowired 字段。
这里发生了两件事。
- 由于您使用的是
MockMvcBuilders.standaloneSetup
,因此 不需要 加载ApplicationContext
。 - Jackson 无法将您的
VersionInfo
对象转换为 JSON 如果它是由 Mockito 创建的模拟(至少不能使用尝试映射所有属性的默认映射规则,包括 Mockito 引入的属性)。
解决方法如下:
public class VersionInfo {
@JsonView(VersionInfo.class)
private String helloWorld;
public String getHelloWorld() {
return helloWorld;
}
public void setHelloWorld(String helloWorld) {
this.helloWorld = helloWorld;
}
}
@RestController
@RequestMapping("/versioninfo")
public class VersionInfoController {
@Autowired
private VersionInfo versionInfo;
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@JsonView(VersionInfo.class)
public VersionInfo getVersionInfo() {
return versionInfo;
}
}
public class VersionInfoControllerTest {
@Mock
private VersionInfo versionInfo;
@InjectMocks
private VersionInfoController versionInfoController;
private MockMvc mockMvc;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(versionInfoController).build();
}
@Test
public void getVersionInfo() throws Exception {
mockMvc.perform(get("/versioninfo").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
总结:
- 使用标准 JUnit 4 测试支持并删除与 Spring TestContext Framework 相关的所有内容,因为您没有使用
ApplicationContext
. - 在
VersionInfo
中使用@JsonView
将 JSON 序列化仅限于VersionInfo
. 中的属性
- 在控制器的
getVersionInfo()
方法上使用@JsonView
来指示 Spring 在调用 Jackson [=57] 时使用 view =]映射器。
请记住,在这种情况下使用 @JsonView
只是必要的,因为您正在使用 Mockito 从控制器方法中模拟 return 值。
此致,
Sam(Spring TestContext Framework 的作者)