软件代码在一些方面很像艺术品,各家的审美标准不同,对同一段代码的看法也就不一样。虽然有很多公认的设计模式和原则,但在具体应用上有时也是公说公有理,婆说婆有理。这里我分享一些通过编写测试得出的度量项目,通过测试代码中体现的问题反过来检查产品代码的问题。
一、枚举的规则意味着枚举的测试
if (CARREFOUR.equals(supplier)) { return EUR; } else if (CENTURY_MART.equals(supplier)) { return CNY; } else { throw new UnsupportedSupplierException(supplier); }
类似这样的条件分支语句非常常见,其测试也并不难,将每种条件都覆盖到即可
@Test public void returnsEurWhenGetCurrenyGivenCarrefour() throws Exception { assertEquals(EUR, target.getCurrencyBy(CARREFOUR)); } @Test public void returnsCnyWhenGetCurrenyGivenCenturyMart() throws Exception { assertEquals(CNY, target.getCurrencyBy(CENTURY_MART)); } @Test(expected=UnsupportedSupplierException.class) public void throwsExceptionWhenGetCurrenyGivenUnknownSupplier() throws Exception { target.getCurrencyBy(WE_DONT_KNOW); }
如果现在要支持一个新的供应商,那么可以增加一个测试用例,然后在条件分支中增加一个else if来实现,也就是说每新增一个供应商,我们就得新增一个测试,如果这种变化的频率比较高(噢,这是好事,说明生意做得不赖),代码和测试就会显得比较笨拙。让我们改进一下代码:
final String currency = currencies.get(supplier); if (currency != null) { return currency; } else { throw new UnsupportedSupplierException(supplier); }
由于使用了Map作为实现,测试分支是固定的了,和供应商的增减无关了。
@Before public void setupTestFixture() { Map<string, String> currencies = new HashMap<String, String>(); currencies.put(MATCHED, CORRESPONSED); currencies.put(ANOTHER, ANOTHER_CURRENCY); target.setCurrencies(currencies); } @Test public void returnsCorresponsedCurrencyWhenSupplierNameMatched() throws Exception { assertEquals(CORRESPONSED, target.getCurrencyBy(MATCHED)); } @Test(expected=UnsupportedSupplierException.class) public void throwsExceptionWhenGetCurrenyGivenUnknownSupplier() throws Exception { target.getCurrencyBy(WE_DONT_KNOW); }
二、静态方法对修改开放,对“测试”关闭
使用静态方法非常简单,无需实例化并传递对象,在哪里都可以直接使用,但如果过度依赖它们,在测试中就不好办了。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response) { if (SessionUtils.isUnexpired(request)) { return true; } else { //在response中输出alert语句 } }
由于在静态方法中对保存在session中的属性做了计算和校验,要对其进行测试就需要事先在session中填充很多信息。
@Test public void returnTrueWhenUserSessionIsNotExpired() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpSession session = new MockHttpSession(); request.setSession(session); session.set......blablabla assertTrue(target.preHandle(request)); } @Test public void printAlertAndreturnFalseWhenUserSessionIsExpired() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpSession session = new MockHttpSession(); request.setSession(session); session.set......blablabla assertFalse(target.preHandle(request, response)); assertEquals(ALERT, response.getContentAsString()); }
由于静态方法属于类,无法通过子类化并覆写的方式在测试时替换,即使测试重点并非SessionUtils.isUnexpired(req)本身,但由于它是一个必经步骤,你不得不为其准备完整的测试数据(哪怕非常麻烦)。让我们对它改造一下吧,不过我也并不想再实现一次isUnexpired(req),耍个小花招吧
再来看测试,就简单多了,我们可以用Stub/mock来替换SessionGateway的实现。
@Test public void returnTrueWhenUserSessionIsNotExpired() throws Exception { context.checking(new Expectations() { { allowing(sessionGateway).isUnexpired(request); will(returnValue(true)); } }); assertTrue(target.preHandle(request, response)); } @Test public void printAlertAndreturnFalseWhenUserSessionIsExpired() throws Exception { context.checking(new Expectations() { { allowing(sessionGateway).isUnexpired(request); will(returnValue(false)); } }); assertFalse(target.preHandle(request, response)); assertEquals(ALERT, response.getContentAsString()); }
小插曲、测试版的为什么组合优于继承
你可能已经见过各种版本的组合优于继承的理论和实例,下面我贡献一个测试版本的 :D
先来看一下template版本的测试方案,我们需要分别对SubTemplate1和SubTemplate2的template()编写测试,并且由于AbstractTemplate的template()依赖SomeDependency和AnotherDependency,所以在以上两个测试中,我们需要使用Stub/Mock将它们替换掉,并为每一个测试准备一整套测试数据,即使它们的implementThis()方法只是return 1 和return 2。
再来看一下strategy版本的测试方案,我们需要为UsingStrategy编写测试,并用Stub/Mock替换Strategy、SomeDependency和AnotherDependency的实现,再分别为Strategy1、Strategy2编写测试。
你看,是不是strategy版本的测试方案的职责更清晰一些,每个测试都只负责被测试目标自己的代码,这种感觉在Template的子类特别多,Template包含的模板代码越是完整的时候越明显。
三、样板代码
有些代码吧,不管开发什么功能都要写的,比如经典的 service-dao,我们来看两个案例:
@Transactional (1) @Override public void cancel(String orderId) { Order order = orderRepository.findBy(orderId); (2) order.cancel(); orderRepository.store(order); (3) } @Transactional (4) @Override public void acknowledge(String orderId) { Order order = orderRepository.findBy(orderId); (5) order.acknowledge(); orderRepository.store(order); (6) }这段代码实现了两个功能,取消订单和确认订单,我们假设其逻辑非常简单,Order对象自己就可以处理,其中(2)、(5) 和 (3)、(6)是配对出现的,由于需要持久化数据,这两步总是免不了的。另外,比较隐蔽的是(1)、(4),这两行代码声明了事务,但很少有人去测试它们,而且经常会忘记添加它们。
@Test public void orderIsCanceledAfterCancelling() throws Exception { final Order order = new OrderFixture().build(); final String orderId = order.getId(); context.checking(new Expectations() { { allowing(orderRepository).findBy(orderId); will(returnValue(order)); oneOf(orderRepository).store(order); } }); target.cancel(orderId); assertTrue(order.isCanceled()); } @Test public void orderIsAcknowledgedAfterAcknowledging() throws Exception { final Order order = new OrderFixture().build(); final String orderId = order.getId(); context.checking(new Expectations() { { allowing(orderRepository).findBy(orderId); will(returnValue(order)); oneOf(orderRepository).store(order); } }); target.acknowledge(orderId); assertTrue(order.isAcknowledged()); }
其实,这两段代码并没有什么分支,要大费周张地测试有些多余。最好能够在满足覆盖率的情况下只测试order,尽可能减少所谓的“service”。利用Command模式,将“service”压缩到一个。
@Test public void orderIsCanceledAfterCancelling() throws Exception { final Order order = new OrderFixture().build(); final CancelOrderCommand command = new CancelOrderCommand(); command.handle(order); assertTrue(order.isCanceled()); } @Test public void orderIsAcknowledgedAfterAcknowledging() throws Exception { final Order order = new OrderFixture().build(); final AcknowledgeOrderCommand command = new AcknowledgeOrderCommand(); command.handle(order); assertTrue(order.isAcknowledged()); }
经过修改后,在测试中我们不用再编写样板代码(同时产品代码中也不用了),由于Order、Command都可以通过构造函数实例化, 测试场景的准备就简单了。而且@Transactional忘记添加的问题也有所缓解(因为只需要一个“service”了)
四、对应代码抽象级别的测试
public void run() { loggingSupport.info("start running, productId=" + productId); if (a) { ...... } else if (b) { ...... } else { ...... } loggingSupport.info("end running, productId=" + productId); }在它的三个测试中,都需要特意指定一下日志记录的内容:
final String startText = "start running, productId" + productId; final String endText = "end running, productId" + productId; context.checking(new Expectations() { { oneOf(loggingSupport).info(startText); // other expectations oneOf(loggingSupport).info(endText); } }); target.run();
出现这种情况的一个原因就是抽象层级没有把握好,在run()方法中,应该是关注的流程,而不是流程节点的实现。比如你在一个测试中编写了很多的断言(对模拟对象的预期定义也可以看作是一种断言),就很有可能说明了这种现象,最好通过引入更细粒度的对象来分担职责。这里我们不妨引入RunnerNotifer,让它来通知记录日志:
public void run() { runnerNotifier.notifyStart(productId); if (a) { ...... } else if (b) { ...... } else { ...... } runnerNotifier.notifyEnd(productId); }这样修改之后,测试代码就不需要再拼接日志内容了:
context.checking(new Expectations() { { oneOf(runnerNotifier).notifyStart(productId); // other expectations oneOf(runnerNotifier).notifyEnd(productId); } }); target.run();
然后再单独为RunnerNotifier的实现添加测试即可。
本次就到这里,如果你有更好的想法,请务必跟贴让我知道,谢谢 :)
相关推荐
总的来说,软件测试度量技术和代码静态检测技术的结合使用,可以帮助开发团队实现早期缺陷发现,提高代码质量,降低维护成本。通过深入理解和熟练运用lofiscope、Pclint、rulechecker、testchecker等工具,开发者...
Java编程语言在软件开发中扮演着重要角色,而软件度量是评估代码质量和复杂性的重要工具。本资源提供了一个简单的Java实现,用于计算代码行数,这是度量软件规模的一个基本指标,也是估算软件开发成本的关键因素。这...
针对Java代码质量度量进行研究,使用Ant工具整合各种开源的静态测试工具,并制定基于静态分析的Java代码质量综合评价方案,可支持包括代码规模、规范性、可维护性、可扩展性和潜在危险等方面的综合检测,为项目的...
《CISQ代码质量度量标准》是软件质量协会推出的一套权威的代码质量衡量体系,旨在提升软件的可靠性和可维护性,优化性能效率,保障安全性。这一标准分为四个核心方面,每一方面都对软件开发过程中的不同维度进行了...
6. **测试覆盖率**:度量代码被自动化测试覆盖的程度,通常使用如JaCoCo或Cobertura等工具进行计算。 7. **重复代码**:度量代码中的重复片段,Deduplication或Clone Detection可以检测并减少代码重复,提高代码...
本资料包包含了一系列与代码质量管理相关的文章和文档,涵盖了多个重要方面,如代码评审、单元测试、程序复杂度度量、IBM的代码质量标准、代码质量概述、C语言编程规范以及静态代码分析理论。以下是对这些知识点的...
Ruby-MetricFu是一个强大的工具,专门用于执行多种代码质量度量和分析,旨在帮助开发者提升代码质量和维护性。这个框架整合了多个代码分析工具,提供了一站式的解决方案来检查Ruby项目中的潜在问题。 MetricFu 包含...
6. **自动化测试度量**:量化自动化测试的程度和效率。 7. **代码覆盖率(Code Coverage)**:度量软件代码被测试覆盖的程度。 8. **单一功能测试验收质量度量**:针对单一功能进行的测试结果评价。 ### 总结 软件...
总的来说,Java软件度量源码的学习能够提升开发者对代码质量的认识,帮助他们编写更健壮、可维护的代码。通过实际操作和研究这些源码,可以提高个人的编程技能,同时也能为团队的软件开发过程带来显著的改进。
微软软件质量测试常用度量 1产品设计规范(SPEC或设计文档)质量状态 2缺陷(BUG)数据有关度量 3测试案例度量 4测试规范度量 ...6自动化测试度量 7CODECOVERAGE(代码覆盖) 8单一功能测试验收质量度量
航天型号软件的复杂性和关键性要求其具备极高的可靠性和安全性,因此,对于软件代码质量的度量评估就显得尤为重要。本文所探讨的《航天型号软件代码质量度量评估实现》,就是基于此背景下产生的,旨在为航天型号软件...
本文探讨了如何通过代码度量来评估和改进代码质量,特别是关注耦合度量——传入耦合和传出耦合,这两种度量对于分析软件架构的稳定性至关重要。 传入耦合(Ca)是指一个模块被其他模块引用的程度,数值越高意味着该...
在IT行业中,尤其是在前端开发领域,测试质量的度量是保证软件质量和提升开发效率的关键环节。本文主要讨论了如何衡量前端测试的质量、效率以及如何找到改进的方向,通过一系列的度量标准和方法来确保测试的有效性和...
实现高质量代码的关键包括遵循编程规范,使用自动化测试工具检测潜在问题,以及实施代码质量度量,如代码复杂度、覆盖率等。此外,持续集成工具可以帮助开发者尽早发现并修复问题。 6. PPT形式的呈现:PPT作为一种...
1. **软件质量度量**:度量是评估软件质量的基础,包括代码质量、测试覆盖率、缺陷密度、变更频率等多个方面。例如,缺陷密度是通过计算每千行代码中的错误数量来衡量代码质量;测试覆盖率则关注了多少代码被测试...
软件代码质量度量项指标。每一条规则都非常详细(包含规则说明、代码示例、参考说明等)。 使用时打开主页面“METRIC.html”即可看到所有编码规则的目录,点击相关条目链接即可跳转至相关规则的详细说明。
总的来说,"metricsanalyzer"提供的Java源码为理解软件度量提供了实践平台,开发者可以通过学习和使用这个工具,提升自身在软件度量方面的能力,从而提高代码质量和项目效率。通过深入研究源码,我们可以了解到如何...
Sonar等工具提供了自动化的代码分析,可以度量代码的重复性、单元测试覆盖率、代码复杂度等,帮助开发者识别潜在的问题,指导优化方向。 通过艾威(AVTECH)的培训,开发者可以掌握这些关键知识点,提升个人技能,...
通过不同的插件对这些结果进行再加工处理,通过量化的方式度量代码质量的变化,从而可以方便地对不同规模和种类的工程进行代码质量管理。同时 Sonar 还对大量的持续集成工具提供了接口支持,可以很方便地在持续集成...