单元测试中的“请求范围已关闭”球衣错误

Posted

技术标签:

【中文标题】单元测试中的“请求范围已关闭”球衣错误【英文标题】:"Request scope has already been shut down" jersey error in unit test 【发布时间】:2016-05-26 07:55:44 【问题描述】:

我正在编写一个使用 WireMock 模拟资源的单元测试。我正在嘲笑我的端点以引发异常,例如:

    stubFor(
            post(urlEqualTo("/myEndpoint"))
            .willReturn(aResponse().withStatus(errorCode)
                    .withHeader("Content-Type", "application/json")
                    .withBody(errorJson)));

这里正在测试的我的客户端类的相关部分是:

import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;

public MyClient() 

    private Client client;
    private String baseUrl;

    ...

    public MyDto createObject(MyDto myDto) throws ClientErrorException 
        String resourcePath = MessageFormat.format("myEndpoint");

        return client.target(baseUrl)
                .path(resourcePath)
                .request(MediaType.APPLICATION_JSON_TYPE)
                .header(CONTENT_TYPE_HEADER, MediaType.APPLICATION_JSON)
                .post(Entity.entity(myDto, MediaType.APPLICATION_JSON), MyDto.class);
    

在我的单元测试中,我尝试使用 junit 的 ExpectedException 来捕获并断言返回的错误,例如:

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void test_returnsError() 
    ...

    thrown.expect(NotAuthorizedException.class);
    thrown.expect(NotAuthorizedExceptionStatusMatcher.hasStatusAndError(401, UNAUTHORISED_ERROR));

    myClient.createObject(new MyDto());

其中 NotAuthorizedExceptionStatusMatcher 是我自己定制的匹配器类:

import javax.ws.rs.NotAuthorizedException;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;

public class NotAuthorizedExceptionStatusMatcher extends TypeSafeMatcher<NotAuthorizedException> 

    public static NotAuthorizedExceptionStatusMatcher hasStatusAndError(int status, ErrorDescription entity) 
        return new NotAuthorizedExceptionStatusMatcher(status, entity);
    

    private final int expectedStatus;
    private final ErrorDescription expectedError;

    private int actualStatus;
    private ErrorDescription actualError;

    private NotAuthorizedExceptionStatusMatcher(int expectedStatus, ErrorDescription expectedError) 
        this.expectedStatus = expectedStatus;
        this.expectedError = expectedError;
    

    @Override
    public boolean matchesSafely(NotAuthorizedException exception) 
        actualStatus = exception.getResponse().getStatus();
        actualError = exception.getResponse().readEntity(ErrorDescription.class);
        return expectedStatus == actualStatus && expectedError.equals(actualError);
    

    @Override
    public void describeTo(Description description) 
        description.appendValue(actualStatus)
                .appendText(" was found instead of ")
                .appendValue(expectedStatus)
                .appendText(" and ")
                .appendValue(actualError)
                .appendText(" was found instead of ")
                .appendValue(expectedError);
    


当我的匹配器尝试执行 exception.getResponse().readEntity(ErrorDescription.class) 时,我收到错误:

java.lang.IllegalStateException: Request scope has been already shut down.
        at jersey.repackaged.com.google.common.base.Preconditions.checkState(Preconditions.java:149)
        at org.glassfish.jersey.process.internal.RequestScope.retrieveCurrent(RequestScope.java:239)
        at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:416)
        at org.glassfish.jersey.client.InboundJaxrsResponse.readEntity(InboundJaxrsResponse.java:108)
        at MyProject.NotAuthorizedExceptionStatusMatcher.matchesSafely(NotAuthorizedExceptionStatusMatcher.java:28)
        at MyProject.NotAuthorizedExceptionStatusMatcher.matchesSafely(NotAuthorizedExceptionStatusMatcher.java:8)
        at org.hamcrest.TypeSafeMatcher.matches(TypeSafeMatcher.java:65)
        at org.hamcrest.core.AllOf.matches(AllOf.java:27)
        at org.hamcrest.DiagnosingMatcher.matches(DiagnosingMatcher.java:12)
        at org.junit.internal.matchers.StacktracePrintingMatcher.matchesSafely(StacktracePrintingMatcher.java:29)
        at org.junit.internal.matchers.StacktracePrintingMatcher.matchesSafely(StacktracePrintingMatcher.java:14)
        at org.hamcrest.TypeSafeMatcher.matches(TypeSafeMatcher.java:65)
        at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:12)
        at org.junit.Assert.assertThat(Assert.java:956)
        at org.junit.Assert.assertThat(Assert.java:923)
        at org.junit.rules.ExpectedException.handleException(ExpectedException.java:252)
        at org.junit.rules.ExpectedException.access$000(ExpectedException.java:106)
        at org.junit.rules.ExpectedException$ExpectedExceptionStatement.evaluate(ExpectedException.java:241)
        at com.github.tomakehurst.wiremock.junit.WireMockRule$1.evaluate(WireMockRule.java:67)
        at org.junit.rules.RunRules.evaluate(RunRules.java:20)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:242)
        at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:137)
        at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
        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:497)
        at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
        at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
        at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
        at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
        at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)

如果我使用 try-catch 块来断言异常,同样的代码似乎可以工作,但我更喜欢 ExpectedException 方法。有什么想法可能是这里的原因吗?我怀疑这是我正在使用的框架的组合,但我不太确定从这里去哪里。

【问题讨论】:

响应正文只能读取一次。我认为问题出在某个地方。在您的匹配器中,您正在尝试readEntity。只是看了一眼代码,但我想这是一个开始寻找的好地方。 我已经读过它,但我查过,我只是想读一次。除非它也在我正在使用的依赖项之一的其他地方被读取。此外,我认为这不能解释为什么当我用 try-catch 块方法替换 throwed.expect 时它似乎有效。我的直觉是,引发问题的 throw.expect 方法是隐含的。 在您的客户端中,这会进行隐式读取。您也许可以在您的客户端中获得Response,而不是dto,然后调用Response.bufferEntity,然后您可以返回response.readEntity(Dto)。我认为这应该可以解决问题 啊,谢谢,我认为您在正确的方向上给了我非常有用的提示!我提供的客户端代码必须简化很多,因为我们大量使用 Java 泛型,所以我不想让提供的代码 sn-p 过于复杂。但是我认为你是绝对正确的 - 我会尽快回复更新以希望确认问题已解决:) 我认为我们有一个基本的设计问题,因为我们的客户端 API 指定了一个明确的对象类型 (MyDto) 作为返回类型。但是,在我们的服务器抛出错误的情况下,它可以返回不同类型的对象 (ErrorDescription)。经过一番讨论后,我认为我们将尝试迁移到 BiConsumer 类型的 API,我认为这更好,因为它允许 API 更明确地向用户说明可以返回的内容,并强制用户处理错误案子。再次感谢您的帮助,我认为我们现在走上了更好的道路! 【参考方案1】:

这有可能是由测试类中的规则顺序引起的。这听起来很奇怪,但如果规则的顺序被解释为 [WireMock, ExpectedException],那么 junit 框架实际上会按照该顺序构建它们。这些规则被设计为测试周围的装饰器,这样每个规则都可以在任何 @Before 块之前和任何 @After 之后采取行动。

所以在上面列出的例子中,规则被解析[WireMock, ExpectedException]那么执行顺序将是:

    WireMock 启动服务器

    ExpectedException 初始化(如果有)

    @执行前

    执行测试

    @执行后

    ExpectedException 评估和操作

    WireMock 关闭服务器

我认为这个案例实际上就是您要寻找的案例。

如果我们切换 WireMock 和 ExpectedException 的顺序:

    ExpectedException 初始化(如果有)

    WireMock 启动服务器

    @执行前

    执行测试

    @执行后

    WireMock 关闭服务器

    ExpectedException 评估和操作

然后,当 ExpectedException 规则被允许评估异常中的响应时,所有服务和状态都已被 Wiremock 销毁。


使用 RuleChain 将保证您的规则顺序,并解决问题(在我能够模拟的范围内)。

交换 passOrderfailOrder 链以查看测试失败!

public class WiremockTest 

    public WireMockRule wireMockRule = new WireMockRule(8089);
    public ExpectedException thrown = ExpectedException.none();    
    @Rule
    public RuleChain passOrder = RuleChain.outerRule(wireMockRule).around(thrown);
    //public RuleChain failOrder = RuleChain.outerRule(thrown).around(wireMockRule);

    @Test
    public void exampleTestExpectConnectionException() throws Exception 
        thrown.expect(ConnectionObjectContainer.class);

        //Custom matcher that gets something from the cached connection after the failure occurs.
        thrown.expect(new BaseMatcher<ConnectionObjectContainer> ()

            public boolean matches(Object item) 
                ConnectionObjectContainer container = (ConnectionObjectContainer) item;
                try 
                    //XXX This line is intended to mimic the NotAuthorizedException class behavior
                    System.out.println(container.getResponseCode());

                 catch (IOException exception) 
                   throw new RuntimeException(exception);
                
                return true;
            

            public void describeTo(Description description) 
                //Not used in sample
            );


        stubFor(get(urlPathMatching("/my/resource/[0-9]+")).willReturn(aResponse().withStatus(200).withHeader(
            "Content-Type", "text/xml").withBody("<response>Some content</response>")));

        URL obj = new URL("http://localhost:8089/my/resource/121");
        HttpURLConnection con = (HttpURLConnection) obj.openConnection();

        // optional default is GET
        con.setRequestMethod("GET");

        throw new ConnectionObjectContainer(con);
    



    /**
     * My Exception class that caches the HttpURLConnection and provides an accessor for the response code to the Matcher of my 
     * ExpectedException configuration.
     * 
     * @since Jun 17, 2016
     *
     */
    public class ConnectionObjectContainer extends Exception 
        HttpURLConnection connection;
        public ConnectionObjectContainer(HttpURLConnection conn) 
            this.connection = conn;
        

       public int getResponseCode() throws IOException 
            return connection.getResponseCode();
        
    



【讨论】:

以上是关于单元测试中的“请求范围已关闭”球衣错误的主要内容,如果未能解决你的问题,请参考以下文章

mocha 单元测试中的 Highchart 导入错误

使用 URLRequestConvertible 的单元测试中的链接错误

如何订阅角度服务单元测试中的错误案例

单元测试错误:无法从同步测试中调用 Promise.then

Laravel 单元测试显示完全错误

Laravel 5.5 中 PHP 单元测试中的类验证器错误