Java基础学习总结(90)——Java单元测试技巧

测试是开发的一个很是重要的方面,能够在很大程度上决定一个应用程序的命运。良好的测试能够在早期捕获致使应用程序崩溃的问题,但较差的测试每每老是致使故障和停机。java

虽然有三种主要类型的软件测试:单元测试,功能测试和集成测试,可是在这篇博文中,咱们将讨论开发人员级单元测试。在我深刻讲述具体细节以前,让咱们先来回顾一下这三种测试的详细内容。数据库

软件开发测试的类型

单元测试用于测试各个代码组件,并确保代码按照预期的方式工做。单元测试由开发人员编写和执行。大多数状况下,使用JUnit或TestNG之类的测试框架。测试用例一般是在方法级别写入并经过自动化执行。服务器

集成测试检查系统是否做为一个总体而工做。集成测试也由开发人员完成,但不是测试单个组件,而是旨在跨组件测试。系统由许多单独的组件组成,如代码,数据库,Web服务器等。集成测试可以发现如组件布线,网络访问,数据库问题等问题。网络

功能测试经过将给定输入的结果与规范进行比较来检查每一个功能是否正确实现。一般,这不是在开发人员级别的。功能测试由单独的测试团队执行。测试用例基于规范编写,而且实际结果与预期结果进行比较。有若干工具可用于自动化的功能测试,如Selenium和QTP。app

如前所述,单元测试可帮助开发人员肯定代码是否正常工做。在这篇博文中,我将提供在Java中单元测试的有用提示。框架

1.使用框架来用于单元测试

Java提供了若干用于单元测试的框架。TestNG和JUnit是最流行的测试框架。JUnit和TestNG的一些重要功能:dom

  • 易于设置和运行。
  • 支持注释。
  • 容许忽略或分组并一块儿执行某些测试。
  • 支持参数化测试,即经过在运行时指定不一样的值来运行单元测试。
  • 经过与构建工具,如Ant,Maven和Gradle集成来支持自动化的测试执行。

EasyMock是一个模拟框架,是单元测试框架,如JUnit和TestNG的补充。EasyMock自己不是一个完整的框架。它只是添加了建立模拟对象以便于测试的能力。例如,咱们想要测试的一个方法能够调用从数据库获取数据的DAO类。在这种状况下,EasyMock可用于建立返回硬编码数据的MockDAO。这使咱们可以轻松地测试咱们意向的方法,而没必要担忧数据库访问。ide

2.谨慎使用测试驱动开发!

测试驱动开发(TDD)是一个软件开发过程,在这过程当中,在开始任何编码以前,咱们基于需求来编写测试。因为尚未编码,测试最初会失败。而后写入最小量的代码以经过测试。而后重构代码,直到被优化。模块化

目标是编写覆盖全部需求的测试,而不是一开始就写代码,却可能甚至都不能知足需求。TDD是伟大的,由于它致使简单的模块化代码,且易于维护。整体开发速度加快,容易发现缺陷。此外,单元测试被建立做为TDD方法的副产品。函数

然而,TDD可能不适合全部的状况。在设计复杂的项目中,专一于最简单的设计以便于经过测试用例,而不提早思考可能会致使巨大的代码更改。此外,TDD方法难以用于与遗留系统,GUI应用程序或与数据库一块儿工做的应用程序交互的系统。另外,测试须要随着代码的改变而更新。

所以,在决定采用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。当测试执行时,它将经过。当代码覆盖率工具运行时,它将显示100%的代码覆盖率,由于 concat 方法中的全部代码都被执行。可是,若是测试执行的值为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;
            }
        }
    }

如下是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

如下是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.使用断言而不是Print语句

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

下面的 StringUti 类是一个简单类,有一个链接两个输入字符串并返回结果的方法:

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

如下是上述方法的两个单元测试:

@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是边界值,对这些值进行测试系统是很是重要的。

相关文章
相关标签/搜索