四、使用Mock对象

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

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

为了使得测试更加容易,有时需要修改代码,如将依赖以成员变量的形式传入被测类中,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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增加一点智能

如这个桩:
1
2
3
4
5
6
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工具简化测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)方法完成注入
下面是示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 @   纪玉奇  阅读(1316)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示