@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 字段。

这里发生了两件事。

  1. 由于您使用的是 MockMvcBuilders.standaloneSetup,因此 不需要 加载 ApplicationContext
  2. 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());
    }

}

总结:

  1. 使用标准 JUnit 4 测试支持并删除与 Spring TestContext Framework 相关的所有内容,因为您没有使用 ApplicationContext.
  2. VersionInfo 中使用 @JsonView 将 JSON 序列化仅限于 VersionInfo.
  3. 中的属性
  4. 在控制器的 getVersionInfo() 方法上使用 @JsonView 来指示 Spring 在调用 Jackson [=57] 时使用 view =]映射器。

请记住,在这种情况下使用 @JsonView 只是必要的,因为您正在使用 Mockito 从控制器方法中模拟 return 值。

此致,

Sam(Spring TestContext Framework 的作者)