2

My spring-mvc app works. Yay! Proof:

enter image description here

This is my good setup:

The important bits of my Buggy-servlet.xml

<import resource="classpath:bug-core.xml" />
<mvc:annotation-driven />
<context:component-scan base-package="buggy.bug" />

The important bits of the bug-core.xml file it imports:

<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />
<bean id="VersionInfoBean" class="buggy.bug.VersionInfo">
    <property name="helloWorld" value="GAHHHHH!!!" />
</bean>

The VersionInfo class:

public class VersionInfo {

    private String helloWorld;

    public String getHelloWorld() {
        return helloWorld;
    }

    public void setHelloWorld(String helloWorld) {
        this.helloWorld = helloWorld;
    }

}

And finally, the 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;
    }

}

It's all good!

Now the problem:

I want to unit test. I think I did good. My 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());
    }
}

The WebAppContext class:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"buggy.bug"})
public class WebAppContext extends WebMvcConfigurerAdapter {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

}

The TestContext class:

@Configuration
public class TestContext {

    @Bean
    public VersionInfo versionInfo() {
        return Mockito.mock(VersionInfo.class);
    }
}

I run junit (either via mvn clean install or in Eclipse junit launch configuration). It's all good.

If I uncomment the two lines indicated in VersionInfoControllerTest, the test may fail or pass (more often it fails). When it fails, it fails in one of two ways:

Way one:

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$10.match(StatusResultMatchers.java:655)
    at org.springframework.test.web.servlet.MockMvc$1.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$1.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$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.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)

Way two is much worse:

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 (StackOverflowError) (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$$41545457["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$$41545457["callbacks"]->org.mockito.internal.creation.MethodInterceptorFilter["handler"]->org.mockito.internal.handler.InvocationNotifierHandler["invocationContainer"]->org.mockito.internal.stubbing.InvocationContainerImpl["invocationForStubbing"]->...

Since it's a StackOverflowError, repeat until out of memory.

I've put up a github project that demonstrates the problem

Any ideas? What did I do wrong? As far as I can tell I did exactly what other accepted answers on SO and blogs, forums, spring docs, all say to do to mock an @Autowired field.

mikerott
  • 423
  • 2
  • 10

1 Answers1

4

There are two things going on here.

  1. Since you're using MockMvcBuilders.standaloneSetup, you do not need to load an ApplicationContext.
  2. Jackson cannot convert your VersionInfo object into JSON if it's a mock created by Mockito (at least not with the default mapping rules that attempt to map all properties including those introduced by Mockito).

Here's the solution:

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());
    }

}

Summary:

  1. Use standard JUnit 4 testing support and delete everything related to the Spring TestContext Framework since you are not using an ApplicationContext.
  2. Use @JsonView in VersionInfo to limit JSON serialization only to properties in VersionInfo.
  3. Use @JsonView on the getVersionInfo() method in your controller to instruct Spring to use the view when invoking the Jackson JSON mapper.

Keep in mind that the use of @JsonView in this scenario is only necessary because you are mocking the return value from a controller method with Mockito.

Regards,

Sam (author of the Spring TestContext Framework)

Sam Brannen
  • 29,611
  • 5
  • 104
  • 136
  • That's it. Thank you so much. I think I finally have a good grasp of the difference between `standaloneSetup` and `webAppContextSetup`. – mikerott Jun 05 '16 at 00:42