Mockito:监视被测系统中依赖HTTP请求的方法是否不好?

时间:2018-12-07 14:38:50

标签: java unit-testing testing mockito

从阅读的内容来看,当您必须监视正在单元测试的当前方法所使用的方法时,通常会发现它是一种不好的做法,并且有代码臭味的迹象。

例如,我有正在单元测试中的这种方法:

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错误,因为我没有提供真实的请求-我相信这就是我想要的,因为我希望我的单元测试能够独立于某些外部系统。

是否有更好的方法进行单元测试?

2 个答案:

答案 0 :(得分:2)

是的,对正在测试的类创建间谍是不好的做法,将要模拟的代码分解为另一个类并对其进行模拟,即:

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");
    }
}

答案 1 :(得分:1)

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

我在测试一个调用被测试对象的另一种方法的方法时注意到的一点是,最好同时执行两种方法的测试。尽管您可以模拟第二种方法,但为了简化第一种方法的测试,您仍然需要回到某个点并测试第二种方法。我个人支持模拟第二种方法(如果它导致更干净的测试代码)。基本上,它是Universe给您的一种选择,不应在所有情况下都排除它。

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

在模拟WebTarget时,在这种情况下,它涉及处理构建器方法,例如.path(someURI).request().post(somejson),每个人都必须相应地加以嘲笑。所以这有点痛苦。如果这样做的话,如果可能的话,我可能会使用集成测试而不是单元测试。也就是说,要使服务器处于连接状态并且可用,以便我可以对其进行测试。在我们的测试环境中,所有服务器都保持运行状态,因此我们可以在单元测试中充分利用更多的集成测试。随着环境的发展,这可能不是一个选择,但是现在可以了,它可以导致更清洁的集成测试,从而在更少的测试中消除了很多代码覆盖率。