Search code examples
springspring-mvchtmlunitspring-testgeb

How can I share a MockMvc session with HtmlUnit?


I am using Spring Test MVC HtmlUnit with Geb to drive functional tests for my Spring MVC application. I would like to check that some session variables are saved properly during an interaction. I tried creating a test controller to return those variables, but HtmlUnit and mvc.perform() are using different sessions. Is there a way to use a single shared session between them?

driver setup:

MockMvc mvc = MockMvcBuilders.webAppContextSetup(ctx)
    .apply(SecurityMockMvcConfigurers.springSecurity())
    .build()

HtmlUnitDriver driver = MockMvcHtmlUnitDriverBuilder.mockMvcSetup(mvc).javascriptEnabled(true).build()

test:

when:
    via ProtectedPage

then:
    // this uses session A
    at LoginPage

and:
    // this uses session B
    println mvc.perform(get('/test/sessionAttributes')).andReturn().response.contentAsString

Solution

  • Summary

    Can I ask why you are trying to do some of your work in MockMvc and some of it in HtmlUnit? It really isn't designed to be used this way. Instead, I would recommend interacting with HtmlUnit to consume the session (as your browser would) and verifying those results.

    The reason this doesn't work is that MockMvc.perform works in isolation. The HtmlUnit integration bridges the MockMvc.perform invocations to ensure they work as you would expect in a browser (i.e. tracking sessions). However, the logic to bridge the the MockMvc.perform invocations is encapsulated.

    MockMvc Behavior

    MockMvc requests work in isolation and will by default use a new session on every request. For example, the following two requests operate on distinct sessions:

    // this uses session A
    mvc.perform(get("/test"))
    
    // this uses session B
    mvc.perform(get("/test"))
    

    In order to reuse the session, you must obtain the session from the first MockMvc.perform invocation and set it on the second the MockMvc.perform invocation. For example:

    MvcResult mvcResult = mvc
            .perform(get("/a"))
            .andReturn();
    
    // reuse the previous session   
    MockHttpSession session = (MockHttpSession) mvcResult
            .getRequest().getSession();
    mvc.perform(get("/b").session(session));
    

    The HtmlUnit support keeps track of the sessions in MockMvcWebConnection and sets the appropriate session (similar to what you saw above) based upon the JSESSIONID cookie.

    In order for you to reuse the HttpSession from the HtmlUnit support in a MockMvc request, you would need access to the original session. However, this logic is encapsulated within the HtmlUnit support and thus you cannot access it.

    Workaround

    I don't expect that we will expose the internals of the HtmlUnit integration. I also would not recommend mixing and matching MockMvc HtmlUnit integration with straight MockMvc usage. However, you can workaround the issue.

    The first step is to create a ResultHandler that tracks the last session.

    public class SessionTracking implements ResultHandler {
        private MockHttpSession lastSession;
    
        @Override
        public void handle(MvcResult result) throws Exception {
            lastSession = (MockHttpSession) result.getRequest().getSession(false);
        }
    
        public MockHttpSession getLastSession() {
            return lastSession;
        }
    }
    

    The next step is to ensure you register SessionTracking with your MockMvc instance.

    SessionTracking sessions = new SessionTracking();
    
    MockMvc mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(SecurityMockMvcConfigurers.springSecurity())
            // ADD THIS
            .alwaysDo(sessions)
            .build();
    
    HtmlUnitDriver driver = MockMvcHtmlUnitDriverBuilder
        .mockMvcSetup(mvc)
        .build();
    

    Now if you need to make a MockMvc request, you can access the previous session using the SessionTracking object.

    when:
        via ProtectedPage
    
    then:
        // this uses session A
        at LoginPage
    
    and:
        // this uses session A
        println mvc.perform(get('/test/sessionAttributes').session(sessions.lastSession).andReturn().response.contentAsString