四、使用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对象的正确性,因此单元测试需要配合端到端的集成测试。