四、使用Mock对象

很多情况下,代码需要与外部依赖打交道,如一个REST地址,数据库链接、外部IO等;这些依赖有些速度过慢、有些不够稳定,不符合单元测试要求的快速、可重复等原则性要求,因此引入了Mock对象这一概念。与Mock相关的还有Stub这个单词。
  • stub 桩,它针对指定的输入缓存了行为
  • mock 模拟对象,增加了对输入条件校验、注入等功能,简单来说,它保证在收到预期参数时表现出预定义的行为,常用的有两个框架
    • mockito 较为易用
    • powermock 功能更加强大,能够对静态方法和私有函数进行Mock
一般来说,在编写stub之后,需要将其注入依赖对象中,也即依赖注入(DI),框架上有Spring DI和Google Guice等。

修改代码结构使其更具可测性

为了使得测试更加容易,有时需要修改代码,如将依赖以成员变量的形式传入被测类中,如:
public class AddressRetriever{
    private Http http; //将外部依赖以构造函数的方式引入,对单元测试更加友好
 
    public AddressRetriever(Http http){
        this.http = http;
    }
 
    public Address retrieve(double latitude, double longitude) throws IOException, ParseException{
        String params = String.format("lat=%.6flon=%.6f", latitude, longitude);
        String response = http.get("http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"+params);
 
        JSONObject obj = (JSONObject)new JSONParse().parse(reponse);
        //...
    }
}
 
但不仅限于构造函数,还可以通过set方法或其他依赖注入框架实现。

为Stub增加一点智能

如这个桩:
Http http = new Http(){
    @Override
    public String get(String uri) throws IOException{
        return "{\"address\":{" + "\"house_number\":\"324\"," + "\"road\":\"North Tejon Street\","
        }
}
这个桩接受任何uri即可返回对应的结果,没有对输入进行判断,我们期望的是:在收到预期参数时提供预期的输入,可以通过在get()方法中加入判断实现,这样的通用功能引入Mock工具。

使用Mock工具简化测试

public class AddressRetrieverTest {
    @Test
    public void answersAppropriateAddressForValidCoordinates() throws IOException, ParseException {
        Http http = mock(Http.class);
        when(http.get(contains("lat=38.000000&lon=-104.000000"))).thenReturn(
            "{\"address\":{" + "\"house_number\":\"324\","
            // ...
            + "}");
 
        AddressRetriever retriever = new AddressRetriever(http);
        Address address = retriever.retrieve(38.0,-104.0);
 
        assertThat(address.houseNumber, equalTo("324"));
    }
when().thenReturn()模式就是Mockito设置的常用方式。

介绍一种DI工具

DI工具有很多,如Spring DI和Google Guice,但是moctito内建的DI工具也能满足绝大部分的需要,步骤如下:
  • 使用@Mock注解创建一个模拟对象
  • 使用@InjectMocks注解声明一个目标对象
  • 在目标对象初始化完毕后,调用MockitoAnnotations.initMocks(this)方法完成注入
下面是示例代码:
public class AddressRetrieverTest{
    @Mock
    private Http http;
 
    @InjectMocks
    private AddressRetriever retriever;
 
    @Before
    public void createRetriever(){
        retriever = new AddressRetriever();
        MockitoAnnotations.initMocks(this);
    }
 
    @Test
    public void answersAppropriateAddressForValidCoordinates() throws IOException, ParseException{
        when(http.get(contains("lat=38.000000&lon=-104.000000")))
            .thenReturn("{\"address\":{" 
                + "\"house_number\":\"324\","
                      //...
           }
 }
最后需要注意的是,如果使用了Mock,那不是直接测试生产代码,而是在于生产代码中加了鸿沟,单元测试的正确性依赖于被Mock对象的正确性,因此单元测试需要配合端到端的集成测试。
posted @ 2020-10-19 17:36  纪玉奇  阅读(1294)  评论(0编辑  收藏  举报