非单元测试的危险

总览

当今,在大多数开发商店中,单元测试是一种广泛接受的实践,尤其是在工具JUnit出现之后。 JUnit是如此有效,并且很早就被使用,只要我记得它就已经包含在eclipse的默认发行版中,并且我从事Java专业编程已有大约8年的时间。 但是,非单元测试的缺点是具体的,并且会不时出现。 本文旨在给出一些非单元测试的风险的具体示例。

单元测试的好处

单元测试具有几项基本的实际好处,减少了未被广泛使用的日子所带来的麻烦。 在不涉及单元测试需求和参数的细节的情况下,让我们简单地强调一下这些好处,因为它们被Java开发专业人员普遍接受,尤其是在敏捷社区中。

  • 自动回归单元测试套件具有按单元隔离错误的能力,因为测试专注于单元并模拟出所有其他依赖项
  • 单元测试会在测试,代码,测试,代码节奏的过程中立即向开发人员反馈
  • 单元测试在生命周期的早期发现缺陷。
  • 单元测试提供了一个安全网,该安全网有助于进行必要的重构以改进代码设计而又不破坏现有功能
  • 单元测试以及代码覆盖率工具可以生成有形的指标,例如代码覆盖率,这对于良好的测试质量而言非常有价值
  • 单元测试提供了一个可执行的示例,说明了客户端代码如何使用代码库的各种接口。
  • 单元测试产生的代码通常更具可读性和简洁性,因为不是那么难于进行单元测试的代码。 因此,随之而来的是,与单元测试一起编写的代码趋向于更加模块化和更高质量。

非单元测试的风险

让我们以示例的方式探讨单元测试如何不会对代码产生不利影响,并允许错误轻松地输入代码库。 重点将放在方法级别上,在该方法级别上,方法简单且直接,但是如果未对代码进行单元测试,仍然会出现问题。

示例1:重用一些代码,但是引入了一个错误

此示例说明了一种情况,开发人员有很好的意图来重用某些代码,但是由于缺少单元测试,因此开发人员无意中引入了一个错误。 如果存在单元测试,则开发人员可以安全地进行重构,并且可以依靠单元测试来通知他某些未满足的要求。

让我们介绍一个简单的场景,其中一家服装店拥有一个系统,让用户输入其服装的销售量。 系统中的两个对象是:Shirt和ShirtSaleValidator。 ShirtSaleValidator检查衬衫,以查看输入的销售价格是否正确。 在这种情况下,衬衫的销售价格必须在0.01美元到15美元之间。 (请注意,此示例过于简化,但仍说明了单元测试的好处。)

编码器Joe实现了isShirtSalePriceValid方法,但未编写任何单元测试。 他正确地遵循了要求。 代码正确。

package com.assarconsulting.store.model;

public class Shirt {

 private Double salePrice;
 private String type;
 
 public Shirt() {
 }

 public Double getSalePrice() {
  return salePrice;
 }

 public void setSalePrice(Double salePrice) {
  this.salePrice = salePrice;
 }

 public String getType() {
  return type;
 }

 public void setType(String type) {
  this.type = type;
 }
}
package com.assarconsulting.store.validator;

import com.assarconsulting.store.model.Shirt;
import com.assarconsulting.store.utils.PriceUtility;

public class ShirtSaleValidator {

 public ShirtSaleValidator() {
 }
 
 public boolean isShirtSalePriceValid(Shirt shirt) {
  
  if (shirt.getSalePrice() > 0 && shirt.getSalePrice() <= 15.00) {
   return true;
  }
  
  return false;
 }
}

编码器Bob出现了,他“重构”了头脑,他喜欢DRY原理,并希望重用代码。 在其他要求期间,他实现了Range对象。 他在衬衫定价要求中也看到了它的用法。 请注意,鲍勃对乔的要求并不十分熟悉,但是对自己足够有能力做出改变感到足够熟悉。 此外,他们的团队遵守集体所有权的极限编程原则。

因此,鲍勃(Bob)高贵地做出了更改以重用一些代码。 他Swift将现有代码转换为使用实用程序方法,并继续满意。

package com.assarconsulting.store.validator;

import com.assarconsulting.store.model.Shirt;
import com.assarconsulting.store.utils.Range;

public class ShirtSaleValidator {

    public ShirtSaleValidator() {
    }
    
    public boolean isShirtSalePriceValid(Shirt shirt) {
                
        Range< Double > range = new Range< Double >(new Double(0), new Double(15));

        if (range.isValueWithinRange(shirt.getSalePrice())) {
            return true;
        }
        
        return false;
    }
}
package com.assarconsulting.store.utils;

import java.io.Serializable;

public class Range< T extends Comparable> implements Serializable
{
    private T lower;
    private T upper;
    
   
    public Range(T lower, T upper)
    {
        this.lower = lower;
        this.upper = upper;
    }
    
    public boolean isValueWithinRange(T value)
    {
        return lower.compareTo(value) <= 0 && upper.compareTo(value) >= 0;
    }

    public T getLower() {
      return lower;
    }
    
    public T getUpper() {
      return upper;
    }
}

由于没有单元测试,因此创建了一个错误,并且从未在实施时捕获。 直到开发人员或用户专门通过UI或其他客户端运行手动测试,该错误才会被注意到。 什么是错误? 新代码允许将0用作衬衫的价格,这不是要求指定的价格。

如果有一组现有的单元测试可以对该要求进行回归测试,则很容易发现这一点。 我们至少可以进行一组简单的测试,以检查衬衫的价格范围。 单元测试集可以在每次代码检入或每个内部版本上运行。 例如,测试服可能声称以下内容。

  • $ 0 =价格执行isShirtSalePriceValid为false
  • $ 0.01 =价格执行isShirtSalePriceValid为true
  • $ 5 =价格执行isShirtSalePriceValid为true
  • $ 15 =价格执行isShirtSalePriceValid为true
  • $ 16 =价格执行isShirtSalePriceValid为false
  • $ 100 =价格执行isShirtSalePriceValid为false

如果Bob依赖这些测试,则第一个项目符号点测试将失败,并且他将立即捕获到自己的错误。

风险–想象一下如果没有单元测试,那么数百个业务需求比这还要复杂。 与安全网和降低的单元测试成本相比,不进行单元测试会导致错误,重复代码和难以维护的复合效果可能是指数级的。

示例2:未经单元测试的代码会产生无法测试的代码,从而导致代码不整洁,难以理解。

让我们继续服装商店系统示例,其中涉及衬衫对象的定价。 该企业希望推出Fall Shirt Sale,可以描述为:

在秋季,如果一件衬衫的价格低于10美元且属于Polo品牌,则可以享受20%的折扣。 秋季销售从2009年9月1日至2009年11月15日止。

计划不编写单元测试的Coder Joe将在ShirtSaleValidator类中实现此功能。 由于测试方法不在他的关注范围之内,因此他不关心使该方法可测试,即,使方法简洁明了以免引入过多的McCabe圈复杂性 。 增加复杂性很难进行单元测试,因为要实现代码覆盖范围,必须使用许多测试用例。 他的代码是正确的,但结果可能如下所示。

package com.assarconsulting.store.validator;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import com.assarconsulting.store.model.Shirt;
import com.assarconsulting.store.utils.PriceUtility;

public class ShirtSaleValidator {

 private Calendar START_FALL_SALE_AFTER = new GregorianCalendar(2009, Calendar.AUGUST, 31);
 private Calendar END_FALL_SALE_BEFORE = new GregorianCalendar(2009, Calendar.NOVEMBER, 16);
 
 public ShirtSaleValidator() {
 }
 
 public boolean isShirtEligibleForFallSaleNotTestable(Shirt shirt) {
  
  Date today = new Date();
  
  if (today.after(START_FALL_SALE_AFTER.getTime()) && today.before(END_FALL_SALE_BEFORE.getTime())) {
   
   if (shirt.getSalePrice() > 0 && shirt.getSalePrice() <= 10 ) {
    
    if (shirt.getType().equals("Polo")) {
     return true;
    }
   }
  }
  
  return false;
 }
}

此代码存在许多问题,包括根据OO原则的逻辑放错和缺少枚举。

但是,撇开这些其他问题,让我们集中讨论此方法的可读性。 仅在很短的时间内看一下代码就很难确定其含义。 开发人员必须研究代码以确定要解决的要求。 这不是最佳的。

现在让我们考虑一下此方法的可测试性。 如果有人要测试Joe的代码,则由于他的NOT单元测试而决定以这种方式离开后,将很难进行测试。 该代码包含3个嵌套的if语句,其中有2个带有“ and”,并且它们最终导致通过代码的许多路径。 此测试的输入将是一场噩梦。 我将这种类型的代码视为不遵循TDD的结果,即编写代码而不打算对其进行测试。

编写此代码的更面向TDD的方式如下。

package com.assarconsulting.store.validator;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import com.assarconsulting.store.model.Shirt;
import com.assarconsulting.store.utils.PriceUtility;

public class ShirtSaleValidator {

 private Calendar START_FALL_SALE_AFTER = new GregorianCalendar(2009, Calendar.AUGUST, 31);
 private Calendar END_FALL_SALE_BEFORE = new GregorianCalendar(2009, Calendar.NOVEMBER, 16);
 
 public ShirtSaleValidator() {
 }
 
 public boolean isShirtEligibleForFallSale(Shirt shirt) {
  
  return isFallSaleInSession() &&
    isShirtLessThanTen(shirt) &&
    isShirtPolo(shirt);

 }

 protected boolean isFallSaleInSession() {
  Date today = new Date();
  return today.after(START_FALL_SALE_AFTER.getTime()) && today.before(END_FALL_SALE_BEFORE.getTime());
 }
 
 protected boolean isShirtLessThanTen(Shirt shirt) {
  
  return shirt.getSalePrice() > 0 && shirt.getSalePrice() <= 10;
 }
 
 protected boolean isShirtPolo(Shirt shirt) {
  return shirt.getType().equals("Polo");
 }
}

从这段代码中,我们可以看到方法isShirtEligibleForFallSale()读取的内容与要求非常相似。 组成它的方法是可读的。 在这些方法中,要求被分解了。 我们可以分别使用2-3种测试方法分别测试需求的每个组成部分。 该代码是干净的,具有一组单元测试,有其正确性的证明和用于重构的安全网。

危险 –编写代码而不进行测试可能会导致结构不良的代码以及难以维护的代码。

结论

以上示例仅是上述单元测试的缺点的简单说明。 非单元测试的危险的总和和复合作用会使系统开发困难且成本高昂。 我希望上面的插图能传达单元测试代码的重要性。

源代码

peril-not-unit-testing.zip

参考: Assant Java Consulting博客上来自我们的JCG合作伙伴 Nirav Assar的GWT和HTML5 Canvas演示


翻译自: https://www.javacodegeeks.com/2012/05/perils-of-not-unit-testing.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值