【问题标题】:Mockito: Is it bad practice to spy on a method within the system under test which relies on a HTTP request?Mockito:监视被测系统中依赖于 HTTP 请求的方法是不好的做法吗?
【发布时间】:2018-12-07 14:38:50
【问题描述】:

通过阅读,当您必须监视您正在单元测试的当前方法所使用的方法时,这通常是一种不好的做法和代码异味的迹象。

例如,我有一个正在单元测试的方法:

public MyResponseObject doStuff(MyRequestObject obj) {
    WebTarget tar = getServiceClient().target(obj.toString());
    Response res = tar.path(someURI).request().post(somejson);
    if(response.getStatus() == 200) {
        String jsonResp = response.readEntity(String.class);
        return convertToObj(jsonResp);
    }
}

我试图解决上述问题的一种方法是通过

  1. 将前两行(WebTarget、Response)提取到它自己的方法中,该方法返回一个 Response 对象。
  2. 创建一个 Response 的模拟并存根 readEntity 以返回 200 和 readEntity 以返回“OK”

结果如下:

public MyResponseObject doStuff(MyRequestObject obj) {
    Response res = sendRequest(obj.toString());
    if(response.getStatus() == 200) {
        String jsonResp = response.readEntity(String.class);
        return convertToObj(jsonResp);
    }
}

//extracted method
public Response sendRequest(String json){
    WebTarget tar = getServiceClient().target(someUrl);
    return res = tar.path(someURI).request().post(somejson);
}

//My unit test

//sut is the system under test, setup elsewhere
public void testDoStuff() {
    MyRequestObject request = ...;
    Response respMock = mock(Response.class);
    when(respMock.getStatus()).thenReturn(200);
    when(respoMock.readEntity()).thenReturn("OK");
    MyClass spy = spy(sut);
    Mockito.doReturn(respMock).when(spy).sendRequest(requestString);
    MyResponseObject  response = spy.doStuff(request);

    assertEquals(response.toString(),expectedResp);
}

如果我不把它存根,它会尝试执行一个真正的 HTTP 请求并返回一个无效的 URL 错误,因为我没有提供一个真正的错误 - 我相信这就是我想要的,因为我希望我的单元测试是独立于某些外部系统。

我应该以更好的方式进行单元测试吗?

【问题讨论】:

  • spy on method that is used by the current method you're unit testing 下是什么意思?
  • 嗨,Oleksandr - 我用更详细的示例更新了我的帖子。希望这至少能澄清一点。
  • 您可以: 1) 重构您的代码 - 将 http 传输层提取到单独的层中。 2)使用集成测试来测试http相关的东西 - 使用wiremock - 它会模拟你的目标服务 - 你的代码将是透明的。

标签: java unit-testing testing mockito


【解决方案1】:

是的,创建一个你正在测试的类的间谍是不好的做法,将你正在模拟的代码分解到另一个类中并模拟它,即:

public class MyClass {

    private final MySender sender;

    public MyClass() {
        this(new DefaultSender());
    }

    public MyClass(MySender sender) {
        this.sender = sender;
    }

    public MyResponseObject doStuff(MyRequestObject obj) {
        Response res = sender.sendRequest(obj.toString());
        if (response.getStatus() == 200) {
            String jsonResp = response.readEntity(String.class);
            return convertToObj(jsonResp);
        }
    }

    public interface MySender {
        Response sendRequest(String json);
    }

    private static class DefaultSender implements MySender {
        public Response sendRequest(String json) {
            WebTarget tar = getServiceClient().target(someUrl);
            return res = tar.path(someURI).request().post(somejson);
        }
    }
}

@RunWith(MockitoJUnitRunner.class)
public class MyClassTest {

    private MyClass testSubject;

    @Mock
    private MySender sender;

    @Mock
    private Response response;

    @Test
    public void testDoStuff() {
        String expectedResp = ...;
        MyRequestObject request = ...;
        MyResponseObject  response = testSubject.doStuff(request);

        assertEquals(response.toString(),expectedResp);
    }

    @Before
    public void setup() {
        testSubject = new MyClass(sender);
        when(sender.sendRequest(anyString()).thenReturn(response);
        when(response.getStatus()).thenReturn(200);
        when(response.readEntity()).thenReturn("OK");
    }
}

【讨论】:

  • 谢谢你的例子!这种模式有名称吗?您有其他建议我可以查找相关问题吗?当您希望能够正确地对方法进行单元测试时,这是最常见的做法吗?
  • 我不知道模式名称,但它确实遵循“单一责任原则”,我们将发送您的请求的责任与理解响应的责任分开。如果您想知道如何编写更好的单元测试,我建议您花一点时间研究 TDD,如果您想编写好的测试,它需要成为您在开发过程中的主要关注点之一,并且如果您将测试放在首位并花费考虑一下代码的可测试性时,您可能会发现您自然倾向于编写更简洁、更易于理解的代码。
【解决方案2】:

监视您正在测试的对象已被告知我是一种不好的做法,但这些警告几乎没有解释支持它。我敢肯定,就像其他任何事情一样,它肯定会被滥用。

我在测试调用被测试对象的另一个方法的方法时注意到,如果您可以同时取消对这两种方法的测试,那会更好。虽然您可以模拟第二种方法,但为了使第一种方法的测试更容易,您仍然需要在某个时候返回并测试第二种方法。如果它导致更清晰的测试代码,我个人支持模拟第二种方法。基本上它是宇宙给你的一个选择,不应该在所有情况下都被排除在外。

在您的场景中,我个人的偏好是模拟 WebTarget 对象,而不是创建第二个内部方法。这样做的主要原因是无论如何您都必须返回并测试第二种方法,所以不妨现在解决它。但是,如果您发现通过将前两行分成它们自己的方法(或类)可以使您的代码更清晰,因为它是多次使用的可重用代码,那么当然可以将其拆分为自己的方法。在这种情况下,代码本身的架构(而不是测试需求的架构)决定了代码结构。

在模拟WebTarget 时,在这种情况下,它涉及处理构建器方法,例如.path(someURI).request().post(somejson),每个都必须相应地被嘲笑。所以这有点痛苦。如果我这样做,如果可能的话,我可能会使用集成测试而不是单元测试。也就是说,让正在联系的服务器可用,以便我可以对其进行测试。在我们的测试环境中,所有的服务器都保持运行,这样我们就可以利用更多的集成测试而不是单元测试。随着环境的发展,这可能不是一种选择,但现在它是一种选择,它可以带来更简洁的集成测试,从而在更少的测试中淘汰大量的代码覆盖率。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-05-04
    • 1970-01-01
    • 1970-01-01
    • 2015-11-07
    • 1970-01-01
    • 2020-05-19
    • 2011-08-20
    • 2018-07-01
    相关资源
    最近更新 更多