重构:改善既有代码的设计_重构代码以实现可测试性:一个示例

重构:改善既有代码的设计

在过去的几周中,从事一个遗留项目的工作给了我很多关于测试的材料,包括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的帮助下一点一点地更改代码,最终可以实现简洁的设计。

这篇文章的完整源代码可以在Github上找到。

翻译自: https://blog.frankel.ch/refactoring-code-testability-example/

重构:改善既有代码的设计

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值