重构:改善既有代码的设计
在过去的几周中,从事一个遗留项目的工作给了我很多关于测试的材料,包括Mockito和PowerMock。 上周,我写了关于滥用PowerMock的文章 。 但是,这并不意味着您永远不要使用PowerMock。 只有当它的用法很普遍时,它才是代码的味道。 在本文中,我想举例说明如何在PowerMock的临时帮助下将旧代码重构为可测试性更高的设计。
让我们以下面的代码为例来检查如何做到这一点:
publicclassCustomersReader{
publicJSONObjectread()throwsIOException{
Stringurl=Configuration.getCustomersUrl();
CloseableHttpClientclient=HttpClients.createDefault();
HttpGetget=newHttpGet(url);
try(CloseableHttpResponseresponse=client.execute(get)){
HttpEntityentity=response.getEntity();
Stringresult=EntityUtils.toString(entity);
returnnewJSONObject(result);
}
}
}
请注意,在第三方库中,我们无法使用Configuration
类。 另外,为了简洁起见,我只关心幸福的道路。 实际的代码在处理故障时可能要复杂得多。
显然,此代码从此配置读取HTTP URL,浏览该URL并返回包装在JSONObject中的输出。 问题在于它很难测试,因此我们最好将其重构为可测试性更高的设计。 但是,重构是巨大的风险,因此我们必须首先创建测试以确保不回归。 最糟糕的是,在这种情况下,单元测试无济于事,因为重构会改变类并破坏现有测试。
在进行任何操作之前,我们需要进行测试以验证现有的行为-可以一起破解的任何东西,即使它们不遵循良好的要求。 有两种选择:
- 伪造品:设置HTTP服务器以应答HTTP客户端和数据库/文件以供配置类读取(取决于确切的实现)
- 嘲弄:创建模拟并像往常一样显示其行为
尽管PowerMock危险,但它比Fakes脆弱且易于设置。 因此,让我们从PowerMock开始,但这只是一个临时措施。 目标是同时完善设计和测试,最后将删除PowerMock。 此测试是一个好的开始:
@RunWith(PowerMockRunner.class)
publicclassCustomersReaderTest{
@MockprivateCloseableHttpClientclient;
@MockprivateCloseableHttpResponseresponse;
@MockprivateHttpEntityentity;
privateCustomersReadercustomersReader;
@Before
publicvoidsetUp(){
customersReader=newCustomersReader();
}
@Test
@PrepareForTest({Configuration.class,HttpClients.class})
publicvoidshould_return_json()throwsIOException{
mockStatic(Configuration.class,HttpClients.class);
when(Configuration.getCustomersUrl()).thenReturn("crap://test");
when(HttpClients.createDefault()).thenReturn(client);
when(client.execute(any(HttpUriRequest.class))).thenReturn(response);
when(response.getEntity()).thenReturn(entity);
InputStreamstream=newByteArrayInputStream("{ \"hello\" : \"world\" }".getBytes());
when(entity.getContent()).thenReturn(stream);
JSONObjectjson=customersReader.read();
assertThat(json.has("hello"));
assertThat(json.get("hello")).isEqualTo("world");
}
}
至此,测试工具就位,设计可以一点一点地改变(以确保不回归)。
第一个问题是调用Configuration.getCustomersUrl()
。 让我们介绍一个服务ConfigurationService
类,作为CustomersReader
类和Configuration
类之间的简单代理。
publicclassConfigurationService{
publicStringgetCustomersUrl(){
returnConfiguration.getCustomersUrl();
}
}
现在,让我们将此服务注入到我们的主类中:
publicclassCustomersReader{
privatefinalConfigurationServiceconfigurationService;
publicCustomersReader(ConfigurationServiceconfigurationService){
this.configurationService=configurationService;
}
publicJSONObjectread()throwsIOException{
Stringurl=configurationService.getCustomersUrl();
// Rest of code unchanged
}
}
最后,让我们相应地更改测试:
@RunWith(PowerMockRunner.class)
publicclassCustomersReaderTest{
@MockprivateConfigurationServiceconfigurationService;
@MockprivateCloseableHttpClientclient;
@MockprivateCloseableHttpResponseresponse;
@MockprivateHttpEntityentity;
privateCustomersReadercustomersReader;
@Before
publicvoidsetUp(){
customersReader=newCustomersReader(configurationService);
}
@Test
@PrepareForTest(HttpClients.class)
publicvoidshould_return_json()throwsIOException{
when(configurationService.getCustomersUrl()).thenReturn("crap://test");
// Rest of code unchanged
}
}
下一步是削减对HttpClients.createDefault()
的静态方法调用的依赖。 为了做到这一点,让我们将此调用委托给另一个类,并将实例注入到我们的实例中。
publicclassCustomersReader{
privatefinalConfigurationServiceconfigurationService;
privatefinalCloseableHttpClientclient;
publicCustomersReader(ConfigurationServiceconfigurationService,CloseableHttpClientclient){
this.configurationService=configurationService;
this.client=client;
}
publicJSONObjectread()throwsIOException{
Stringurl=configurationService.getCustomersUrl();
HttpGetget=newHttpGet(url);
try(CloseableHttpResponseresponse=client.execute(get)){
HttpEntityentity=response.getEntity();
Stringresult=EntityUtils.toString(entity);
returnnewJSONObject(result);
}
}
}
最后一步是完全删除PowerMock。 非常简单:
@RunWith(MockitoJUnitRunner.class)
publicclassCustomersReaderTest{
@MockprivateConfigurationServiceconfigurationService;
@MockprivateCloseableHttpClientclient;
@MockprivateCloseableHttpResponseresponse;
@MockprivateHttpEntityentity;
privateCustomersReadercustomersReader;
@Before
publicvoidsetUp(){
customersReader=newCustomersReader(configurationService,client);
}
@Test
publicvoidshould_return_json()throwsIOException{
when(configurationService.getCustomersUrl()).thenReturn("crap://test");
when(client.execute(any(HttpUriRequest.class))).thenReturn(response);
when(response.getEntity()).thenReturn(entity);
InputStreamstream=newByteArrayInputStream("{ \"hello\" : \"world\" }".getBytes());
when(entity.getContent()).thenReturn(stream);
JSONObjectjson=customersReader.read();
assertThat(json.has("hello"));
assertThat(json.get("hello")).isEqualTo("world");
}
}
无论在模拟静态方法还是在运行程序中,都没有任何PowerMock痕迹。 根据我们最初的目标,我们实现了100%易于测试的设计。 当然,这是一个非常简单的示例,真实的代码要复杂得多。 但是,通过在PowerMock的帮助下一点一点地更改代码,最终可以实现简洁的设计。
翻译自: https://blog.frankel.ch/refactoring-code-testability-example/
重构:改善既有代码的设计