Unit-Test einer vom Anforderungskontext abhängigen Methode

71

Ich schreibe einen Komponententest für eine Methode, die die folgende Zeile enthält:

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

Ich erhalte folgende Fehlermeldung:

java.lang.IllegalStateException: Keine threadgebundene Anforderung gefunden: Verweisen Sie auf Anforderungsattribute außerhalb einer tatsächlichen Webanforderung oder verarbeiten Sie eine Anforderung außerhalb des ursprünglich empfangenden Threads? Wenn Sie tatsächlich innerhalb einer Webanforderung arbeiten und diese Nachricht weiterhin erhalten, wird Ihr Code wahrscheinlich außerhalb von DispatcherServlet / DispatcherPortlet ausgeführt: Verwenden Sie in diesem Fall RequestContextListener oder RequestContextFilter, um die aktuelle Anforderung verfügbar zu machen.

Der Grund liegt auf der Hand - ich führe den Test nicht in einem Anforderungskontext aus.

Die Frage ist, wie ich eine Methode testen kann, die einen Aufruf einer vom Anforderungskontext abhängigen Methode in einer Testumgebung enthält.

Vielen Dank.

Satoshi
quelle

Antworten:

191

Spring-Test hat ein flexibles Anforderungsmodell namens MockHttpServletRequest.

MockHttpServletRequest request = new MockHttpServletRequest();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
Brei
quelle
Möglicherweise müssen Sie javax.servlet eine explizite Abhängigkeit hinzufügen: javax.servlet-api, damit das oben Gesagte kompiliert werden kann (Spring 5 zieht das nicht ein)
djb
42

Sie können das RequestAttributesObjekt verspotten / stubben, um das zurückzugeben, was Sie möchten, und dann RequestContextHolder.setRequestAttributes(RequestAttributes)mit Ihrem Mock / stub aufrufen, bevor Sie mit dem Test beginnen.

@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...
}
nicholas.hauschild
quelle
5

Ich konnte das Gleiche wie diese Antwort tun , jedoch ohne die MockHttpServletRequestKlasse, die die @MockAnmerkung verwendet. Ich denke, sie sind ähnlich. Einfach hier für zukünftige Besucher posten.

@Mock
HttpServletRequest request;

@Before
public void setup() {
    MockitoAnnotations.initMocks(this);
    RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
}
maazadeeb
quelle
2

Angenommen, Ihre Klasse ist so etwas wie:

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

Wenn Sie nicht in der Lage sind, die verwendete Klasse zu ändern RequestContextHolder, können Sie die RequestContextHolderKlasse in Ihrem Testcode überschreiben . Das heißt, Sie erstellen eine Klasse mit demselben Namen im selben Paket und stellen sicher, dass sie vor der eigentlichen Spring-Klasse geladen wird.

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.
    }
}

Wenn Ihre Tests ausgeführt werden, nehmen sie Ihre RequestContextHolderKlasse auf und verwenden diese vor der Spring-Klasse (vorausgesetzt, der Klassenpfad ist dafür eingerichtet). Dies ist keine besonders gute Möglichkeit, Ihre Tests zum Laufen zu bringen, aber es kann erforderlich sein, wenn Sie die zu testende Klasse nicht ändern können.

Alternativ können Sie das Abrufen der Sitzungs-ID hinter einer Abstraktion verbergen. Stellen Sie zum Beispiel eine Schnittstelle vor:

public interface SessionIdAccessor {
    public String getSessionId();
}

Erstellen Sie eine Implementierung:

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

Und verwenden Sie die Abstraktion in Ihrer Klasse:

class ClassToTest {
    SessionIdAccessor sessionIdAccessor;

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

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

Dann können Sie eine Dummy-Implementierung für Ihre Tests bereitstellen:

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

Diese Art von Dingen hebt eine übliche Best Practice hervor, um bestimmte Umgebungsdetails hinter Abstraktionen zu verbergen, damit Sie sie austauschen können, wenn sich Ihre Umgebung ändert. Dies gilt auch, um Ihre Tests weniger spröde zu machen, indem Sie Dummy-Implementierungen gegen "echte" austauschen.

Paul Grime
quelle
Vielen Dank, @PaulGrime. Die zweite Lösung ist die, die ich in Betracht gezogen habe, aber ich suchte nach einer Lösung, bei der ich meinen Hauptcode nicht ändern musste ... Ich werde diese Lösung verwenden, wenn niemand eine andere bessere Lösung vorschlägt!
Satoshi
1

Wenn die Methode enthält:

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

Ist die Web-Controller-Methode, dann würde ich empfehlen, die Methodensignatur so zu ändern, dass Sie / spring die Anforderung als separaten Paremter an die Methode übergeben.

Dann können Sie den Unruhestifter-Teil String entfernen RequestContextHolder.currentRequestAttributes()und den HttpSessiondirekt verwenden.

Dann sollte es sehr einfach sein, ein verspottetes Session ( MockHttpSession) -Objekt im Test zu verwenden.

@RequestMapping...
public ModelAndView(... HttpSession session) {
    String id = session.getId();
    ...
}
Ralph
quelle