Unit testing a method dependent to the request con

2020-02-07 18:22发布

I'm writing a unit test for a method that contains the following line:

String sessionId = RequestContextHolder.currentRequestAttributes().getSessionId();

I get the following error:

java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

The reason is quite obvious — I'm not running the test in a request context.

The question is, how can I test a method that contains a call to a method dependent to the request context in a test environnment?

Thank you very much.

5条回答
一夜七次
2楼-- · 2020-02-07 19:02

I was able to do the same as this answer, but without the MockHttpServletRequest class using the @Mock annotation. I guess they're similar. Just posting here for future visitors.

@Mock
HttpServletRequest request;

@Before
public void setup() {
    MockitoAnnotations.initMocks(this);
    RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
}
查看更多
时光不老,我们不散
3楼-- · 2020-02-07 19:08

You can mock/stub the RequestAttributes object to return what you want, and then call RequestContextHolder.setRequestAttributes(RequestAttributes) with your mock/stub before you start your test.

@Mock
private RequestAttributes attrs;

@Before
public void before() {
    MockitoAnnotations.initMocks(this);
    RequestContextHolder.setRequestAttributes(attrs);

    // do you when's on attrs
}

@Test
public void testIt() {
    // do your test...
}
查看更多
倾城 Initia
4楼-- · 2020-02-07 19:11

Assuming your class is something like:

class ClassToTest {
    public void doSomething() {
        String sessionId = RequestContextHolder.currentRequestAttributes().getSessionId();
        // Do something with sessionId
    }
}

If you don't have the ability to change the class that uses RequestContextHolder, then you could override the RequestContextHolder class in your test code. I.e. you create a class with the same name, in the same package, and ensure it is loaded before the actual Spring class.

package org.springframework.web.context.request;

public class RequestContextHolder {
    static RequestAttributes currentRequestAttributes() {
        return new MyRequestAttributes();
    }

    static class MyRequestAttributes implements RequestAttributes {
        public String getSessionId() {
            return "stub session id";
        }
        // Stub out the other methods.
    }
}

Now, when your tests run, they will pick up your RequestContextHolder class and use that in preference to the Spring one (assuming the classpath is set up for this to happen). This isn't a particular nice way of getting your tests to run, but it might be necessary if you can't change the class you are testing.

Alternatively, you could hide the session id retrieval behind an abstraction. For example introduce an interface:

public interface SessionIdAccessor {
    public String getSessionId();
}

Create an implementation:

public class RequestContextHolderSessionIdAccessor implements SessionIdAccessor {
    public String getSessionId() {
        return RequestContextHolder.currentRequestAttributes().getSessionId();
    }
}

And use the abstraction in your class:

class ClassToTest {
    SessionIdAccessor sessionIdAccessor;

    public ClassToTest(SessionIdAccessor sessionIdAccessor) {
        this.sessionIdAccessor = sessionIdAccessor;
    }

    public void doSomething() {
        String sessionId = sessionIdAccessor.getSessionId();
        // Do something with sessionId
    }
}

Then you can provide a dummy implementation for your tests:

public class DummySessionIdAccessor implements SessionIdAccessor {
    public String getSessionId() {
        return "dummy session id";
    }
}

This sort of thing highlights a usual best-practice to hide certain environmental details behind abstractions so that you can swap them out if your environment changes. This applies equally to making your tests less brittle by swapping dummy implementations for 'real' ones.

查看更多
等我变得足够好
5楼-- · 2020-02-07 19:12

Spring-test has a flexible request mock called MockHttpServletRequest.

MockHttpServletRequest request = new MockHttpServletRequest();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
查看更多
爷的心禁止访问
6楼-- · 2020-02-07 19:17

If the method containing:

String sessionId = RequestContextHolder.currentRequestAttributes().getSessionId();

is the web controller method, then I would recommend to change the method signature, so that you/spring pass the Request as an seperate paremter to the method.

Then you can remove the troublemaker part String RequestContextHolder.currentRequestAttributes() and use the HttpSession direcly.

Then it should be very easy to use a mocked Session (MockHttpSession) object in the test.

@RequestMapping...
public ModelAndView(... HttpSession session) {
    String id = session.getId();
    ...
}
查看更多
登录 后发表回答