写好单元测试的7个要点

写好单元测试的7个要点

测试是开发的一个非常重要的方面,可以在很大程度上决定应用程序的命运。好的测试可以在早期捕获应用程序终止问题,但是糟糕的测试总是会导致失败和停止。

虽然有三种主要的软件测试类型:单元测试、功能测试和集成测试,但在本文中,我将讨论开发人员级的单元测试。在我深入研究细节之前,让我们从高层次上回顾一下每种类型的测试需要什么。

软件测试的类型

单元测试 用于测试单个代码组件,并确保代码按预期方式工作。单元测试由开发人员编写和执行。大多数情况下,会使用JUnit或TestNG这样的测试框架。测试用例通常在方法级别编写,并通过自动化执行。

集成测试 检查整个系统是否工作正常。集成测试也是由开发人员完成的,但它不是测试单个组件,而是旨在跨组件进行测试。系统由许多单独的组件组成,如代码、数据库、Web服务器等。集成测试能够发现组件的连接、网络访问、数据库问题等问题。

功能测试 通过将给定输入的结果与规范进行比较来检查每个特性是否正确实现。通常,这不是在开发人员级别完成的。功能测试由单独的测试团队执行。根据规范编写测试用例,并将实际结果与预期结果进行比较。有几种工具可用于自动化功能测试,如Selenium和qtp。

TIPS

1. 使用单元测试框架

Java提供了用于单元测试的若干框架。testng和junit是最流行的测试框架。JUnit和TESTNG的一些重要特性:

  • 易于安装和运行。支持批注。
  • 允许忽略或分组某些测试并一起执行 。
  • 支持参数化测试,即通过在运行时指定不同的值来运行单元测试
  • 通过与Ant、Maven和Gradle等构建工具集成,支持自动测试执行。

EasyMock是一个模拟框架,它是对诸如JUnit和TestNG这样的单元测试框架的补充。easymock本身不是一个完整的框架。它只是增加了创建模拟对象以方便测试的能力。例如,我们要测试的方法可以调用从数据库获取数据的DAO类。在这种情况下,easymock可以用来创建返回硬编码数据的mockdao。这使得我们可以轻松地测试我们想要的方法,而不必为数据库访问而烦恼。

2. 强烈建议使用测试驱动开发!

测试驱动开发(TDD)是一个软件开发过程,在这个过程中,在任何编码开始之前,测试都是根据需求编写的。由于还没有代码,测试最初将失败。然后写入最小数量的代码以通过测试。然后重构代码,逐步优化。

其目标是编写涵盖所有需求的测试,而不是简单地先编写甚至可能不满足需求的代码。TDD非常好,因为它编写了易于维护的简单模块化代码。总体开发速度加快,缺陷容易识别。此外,单元测试是作为TDD方法的副产品创建的。

但是,TDD可能不适用于所有情况。在设计复杂的项目中,专注于最简单的设计以通过测试用例,而不提前考虑可能会导致巨大的代码更改。此外,对于与遗留系统、GUI应用程序或与数据库一起工作的应用程序交互的系统,TDD方法也很难使用。此外,测试需要随着代码的变化而更新。

因此,在决定采用TDD方法之前,应牢记上述因素,并根据项目的性质鉴别是否使用。

3. 评估代码覆盖率

代码覆盖率度量在运行单元测试时执行代码的百分比。通常,具有高覆盖率的代码包含未检测到的错误的可能性会降低,因为在测试过程中执行了更多的源代码。衡量代码覆盖率的一些最佳实践包括:

  • 使用代码覆盖工具,如Clover、Corbetura、Jacoco或Sonar。
  • 使用工具可以提高测试质量,因为这些工具可以指出代码中未测试的部分,从而允许您开发额外的测试来覆盖这些区域。
  • 每当编写新功能时,立即编写要覆盖的新测试。
  • 确保有覆盖代码所有分支的测试用例,即if/else语句。

高代码覆盖率不能保证测试是完美的,所以要小心!下面的concat方法接受一个布尔值作为输入,并且只在布尔值为true时附加传入的两个字符串:

     public String concat(boolean append, String a,String b) {
        String result = null;
        If (append) {
            result = a + b;
                            }
        return result.toLowerCase();
    }

以下是上述方法的测试用例:

        @Test
        public void testStringUtil() {
         String result = stringUtil.concat(true, "Hello ", "World");
         System.out.println("Result is "+result);
        }

在这种情况下,测试的执行值为true。当测试执行时,它将通过。当代码覆盖率工具运行时,它将在执行concat方法中的所有代码时显示100%的代码覆盖率。但是,如果使用值false执行测试,则将引发NullPointerException。因此,100%的代码覆盖率并不能真正表明测试是否覆盖了所有场景,并且测试是否良好。

4. 尽可能将测试数据外置化

在JUnit4之前,运行测试用例的数据必须硬编码到测试用例中。这创建了一个限制,为了用不同的数据运行测试,必须修改测试用例代码。然而,junit4和testng支持将测试数据外部化,以便可以针对不同的数据集运行测试用例,而不必更改源代码。

下面的MathChecker类具有检查数字是否为奇数的方法:

   public class MathChecker {
        public Boolean isOdd(int n) {
            if (n%2 != 0) {
                return true;
            } else {
                return false;
                                         }
        }
    }

TestNG
以下是MathChecker类使用testNg的测试用例:

        public class MathCheckerTest {
        private MathChecker checker;
        @BeforeMethod
        public void beforeMethod() {
          checker = new MathChecker();
        }
        @Test
        @Parameters("num")
        public void isOdd(int num) { 
          System.out.println("Running test for "+num);
          Boolean result = checker.isOdd(num);
          Assert.assertEquals(result, new Boolean(true));
        }
    }

下面是testng.xml(testng的配置文件),它具有要为其执行测试的数据:

   <?xml version="1.0" encoding="UTF-8"?>
    <suite name="ParameterExampleSuite" parallel="false">
    <test name="MathCheckerTest">
    <classes>
      <parameter name="num" value="3"></parameter>
      <class name="com.stormpath.demo.MathCheckerTest"/>
    </classes>
     </test>
     <test name="MathCheckerTest1">
    <classes>
      <parameter name="num" value="7"></parameter>
      <class name="com.stormpath.demo.MathCheckerTest"/>
    </classes>
     </test>
    </suite>

正如你看到的,在这种情况下,测试将执行两次,分别针对值3和7执行一次。除了通过XML配置文件指定测试数据外,还可以通过DataProvider注释在类中提供测试数据。

JUnit
与testng类似,测试数据也可以为junit外部化。以下是上述同一MathChecker类的JUnit测试用例:

    @RunWith(Parameterized.class)
    public class MathCheckerTest {
     private int inputNumber;
     private Boolean expected;
     private MathChecker mathChecker;
     @Before
     public void setup(){
         mathChecker = new MathChecker();
     }
        // Inject via constructor
     public MathCheckerTest(int inputNumber, Boolean expected) {
         this.inputNumber = inputNumber;
         this.expected = expected;
     }
     @Parameterized.Parameters
     public static Collection<Object[]> getTestData() {
         return Arrays.asList(new Object[][]{
                 {1, true},
                 {2, false},
                 {3, true},
                 {4, false},
                 {5, true}
         });
     }
     @Test
     public void testisOdd() {
         System.out.println("Running test for:"+inputNumber);
         assertEquals(mathChecker.isOdd(inputNumber), expected);
     }
 }

可以看到,要为其执行测试的测试数据是由gettestdata()方法指定的。这种方法可以很容易地修改为从外部文件读取数据,而不是使用硬编码数据。

5. 使用断言而不是打印语句

许多新开发人员习惯于在每行代码后编写System.out.println语句,以验证代码是否正确执行。这种实践通常扩展到单元测试,导致测试代码混乱。除了混乱之外,这还需要开发人员手动干预以验证控制台上打印的输出,以检查测试是否成功运行。更好的方法是使用自动指示测试结果的断言。

以下StringUtil类是一个简单类,其中一个方法连接两个输入字符串并返回结果:

    public class StringUtil {
        public String concat(String a,String b) {
            return a + b;
        }
    }

The following are two unit tests for the method above:

    @Test
    public void testStringUtil_Bad() {
         String result = stringUtil.concat("Hello ", "World");
         System.out.println("Result is "+result);
    }
    @Test
    public void testStringUtil_Good() {
         String result = stringUtil.concat("Hello ", "World");
         assertEquals("Hello World", result);
    }  

teststringutil \_bad将始终通过,因为它没有断言。开发人员需要在控制台手动验证测试的输出。如果方法返回错误的结果并且不需要开发人员干预,则teststringutil \_good将失败。

6. 生成具有确定性结果的测试

有些方法没有确定的结果,即该方法的输出不是预先知道的,并且每次都可能变化。例如,考虑具有复杂函数的以下代码和计算执行复杂函数所需时间(毫秒)的方法:

    public class DemoLogic {
    private void veryComplexFunction(){
        //This is a complex function that has a lot of database access and is time consuming
        //To demo this method, I am going to add a Thread.sleep for a random number of milliseconds
        try {
            int time = (int) (Math.random()*100);
            Thread.sleep(time);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public long calculateTime(){
        long time = 0;
        long before = System.currentTimeMillis();
        veryComplexFunction();
        long after = System.currentTimeMillis();
        time = after - before;
        return time;
    }
    }

在这种情况下,每次执行calculateTime方法时,它将返回不同的值。为这个方法编写测试用例没有任何用处,因为方法的输出是可变的。因此,测试方法将无法验证任何特定执行的输出。

7. 测试错误情景和边界情景,以及正常情景

通常,开发人员花费大量的时间和精力编写测试用例,以确保应用程序按预期工作。然而,测试错误的测试用例也是很重要的。否定测试用例是测试系统是否可以处理无效数据的测试用例。例如,考虑一个简单的函数,它读取由用户键入的长度为8的字母数字值。除了字母数字值之外,还应测试以下异常情景测试用例:

  • 用户指定非字母数字值,如特殊字符
  • 用户指定空值。
  • 用户指定的值大于或小于8个字符。

类似地,边界测试用例测试系统是否能很好地处理极端值。例如,如果希望用户输入1到100之间的数值,1和100是边界值,测试系统中这些值非常重要。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容