软件开发测试:单元测试和路径覆盖测试详细指南

2021年3月8日16:37:46 发表评论 1,308 次浏览

本文概述

软件测试概述

关于软件测试,它和用Git一样类似,比如有些程序员不习惯用Git或编写测试用例。我刚开始学编程的时候,测试同样也是学了,就是不知道它有个什么用,还有个测试驱动开发,一脸蒙蔽。测试不就是写一段代码就执行一遍么?或者带有UI的程序,自己点击两下测试一下,当然还遇到一些人喜欢静态分析代码的——像考试写完考卷回头静态分析。

所以我开始也是编写单元测试的,后来才放弃这样编写程序,当认为自己写的代码无比正确的时候——运行起来多数情况都会翻车,这是多么烦躁的事情,有时候都在怀疑自己是不是会写程序的,这么笨,写个简单的函数都错。

如果你会一点数据结构和算法,基本上你可以写一个还可以运行的程序出来,但是大部分时候还是在测试和调试,让程序尽量能出来更多情况的问题。这里就不多说明为什么单元测试是必要的——你可以用自己的时间和经验来证明,但我还是建议:节省时间,初学者即使不懂,模仿一下也无妨,有益无害。

软件测试主要分为黑盒测试和白盒测试,上面说的点击UI就是黑盒测试,静态分析代码属于白盒测试,但还是比较低能的。标准来说,通过操作UI来进行黑盒测试,你同样需要预先设计一下,比如输入值的范围和输出预期值。

其中黑盒测试的方法包括:边界值、等价法、错误推测法、功能分解法、因果图、判定表、正交试验法和场景法。

白盒测试的方法包括:代码检查法、静态结构分析、静态质量度量、逻辑覆盖、基本路径测试、域测试、符号测试、Z路径覆盖和程序变异。

黑盒测试主要是根据项目功能需要来测试的,就是看看程序有没有实现预定的功能,是比较宏观的层面的。白盒测试的是微观上的功能,就是实现程序需求后面的具体实现代码,具体到每一行代码上。

测试方法很多,但是我认为并不需要全部都用上,软件的功能可能很复杂都有,在黑盒测试中只要知道输入数据集和输出数据集就可以了,这些数据集需要匹配程序的需求,并且测试是否有预期的输出,而这里说的方法根据不同的软件,你可能无意中都用上了。而对于黑盒测试,除了测试程序是否符合需求功能的预期,其准确度相对白盒测试来说是比较低的。

这里我想侧重讨论白盒测试,其中路径覆盖又是其中一个常用而比较准确的测试方法,而我认为,写一下代码写一下这种测试基本就够了,并且是必要的,你大概能保证程序80%的情况都是正确的——当然,就算你用完所有方法,随着不断的更新程序,你基本不可能写出一个100%完美的程序,所以对于测试用例的数量——适量就好!

单元测试和路径覆盖

我们在编写程序代码的时候,对这段代码进行测试——一般测试的基本单元是一个函数,有人说测试一个函数也不准确呀,你还依赖其它函数呢!——照这样推理,那你还写不写代码的?你写的代码也不准确呢!其它第三方依赖函数不要管!当它们是100%正确就是了,有错的时候再说(而且第三方依赖,对于标准的库,人家都是测试过了的,比如JDK,或者说okhttp这样的库)。

一个函数由多个语句组成,这些语句又分为无分支语句和条件语句(分支语句),我们可以把一段连续的无分支语句表示成一个结点(Node,或节点,还记得数据结构中的节点吗?),两个语句(两个结点)使用有向箭头连接,表示从第一个语句顺序执行到第二个语句:

单元测试和路径覆盖

这个图又像什么?图论算法呢!有没有记得或学过?

路径测试的详细步骤可以参考:

  • 首先确定测试和分析的代码,一般是一个函数。
  • 根据代码画出程序执行的流程图,就是类似上面这个图。
    • 一段无分支连续语句的代码作为一个结点,单个语句可作为一个结点。
    • 带有条件表达式的代码作为一个结点(如if/while/for/do-while),复杂的逻辑表达式需要分解成单个判断的形式。
    • 代码执行结束为一个结点。
    • 一个条件表达式产生两个分支,复杂组合的条件判断需要分解成单个条件判断。
  • 然后根据这个流程图计算环路复杂度
    • 环路复杂度描述程序的逻辑复杂性。
    • 它的数值表示每条分支至少执行一次的测试用例数量上限。
    • 环路复杂度的计算公式有三个:V(G)=E-N+2,其中E为边数,N为结点数;V(G)=P+1,P为判定结点数,就是产生分支的结点;V(G)=A,A为闭合区域数,边和节点围起来叫做一个区域,图形外也算一个区域;如上图的区域数为A=3,判断结点数P=2,边数E=7,结点数N=6,所以环路复杂度为V(G)=3,表示测试这个函数,我们编写至少三个用例就够了。
  • 接着确定所有路径。
    • 由V(G)=3可以得出,这个程序有三条互不相同的路径。
    • 这些路径分别是:1-2-4-6,1-3-4-6,1-3-5-6。
  • 最后根据每条路径设计测试用例。
    • 设计每条路径的输入和预期输出(输入集和输出集),确保覆盖每条路径。
    • (可选)当前路径输入值的范围(包括正确的范围和错误的范围),确保覆盖每条路径输入的多数值(如基本数据类型、数组、引用类型对应的所有可能值)。

下面我们使用具体的实例来学习路径覆盖测试,其实黑盒测试或白盒测试中的其它方法也是类似的,明白其中的一些要点就行了,比如输入数据集和输出数据集,测试程序的输入和预期输出是对应的,那么就是正确的。

确定测试和分析的代码

这里假设我们设计一个计算器中的一个叠加函数,下面是Java的具体代码(尽管可以使用数学公式快速计算出来,但这里还是用while循环,展示一下常见的模式):

public class Caculator {

    /**
     * 计算从整数s开始到e结束,它们相加的和(不包含e)
     * */
    public int add(int s, int e) throws Exception {
        if(s == e)
            return 0;
        if(s > e)
            throw new Exception("s is greater than e.");
        int sum = 0;
        while(s < e) {
            sum += s;
            s++;
        }
        return sum;
    }

}

根据代码画出程序执行流程图

首先,这段代码有三个条件判断。函数开始执行没有非条件语句,但是我们可以把它看作是执行了一个空语句,也就是说这里有一个结点,然后每个条件判断为一个结点,并且这个几点有两个分支,对于抛出异常我们可以把它等同于结束结点。

该程序的流程图为:

程序流程图和执行路径

其中,第四个结点表示语句“int sum = 0;”,这个结点其实也可以和第5个结点合并一起,但是这样会更清晰一点。

计算环路复杂度

由上图,我们可以得到E=9,N=7,P=3,A=4,所以V(G)=9-7+2=3+1=4。

也就是说,我们至少编写4个测试用例就能实现全部路径覆盖了。

确定所有路径

这些路径包括:

  • 1-2-3-4-5-6-5-7
  • 1-2-3-4-5-7
  • 1-2-3-7
  • 1-2-7

根据每条路径设计测试用例

根据我们的代码,我们可以分析出1-2-3-4-5-7这条路径是绝对不会执行的,就不需要编写这条路径的测试用例了,也就是说,这个while循环至少执行一次,下面是为每条路径设计的输入和输出数据:

  • 1-2-3-4-5-6-5-7:(s=-1,e=10)=>(44)
  • 1-2-3-7:(s=100,e=10)=>(Exception)
  • 1-2-7:(s=e=8)=>0

下面我们用junit编写测试用例:

public class CaculatorTest {

    @Test
    public void shouldReturn44() throws Exception {
        Caculator c = new Caculator();
        Assert.assertEquals(44, c.add(-1, 10));
    }

    @Test
    public void shouldReturn0() throws Exception {
        Caculator c = new Caculator();
        Assert.assertEquals(0, c.add(8, 8));
    }

    @Test(expected = Exception.class)
    public void shouldThrowException() throws Exception {
        Caculator c = new Caculator();
        c.add(100, 10);
    }
}

关于测试函数的命名方式,我建议也要有个标准的命名,比如如果规定一个类对应一个测试用例,一个类可能会有多个函数,我们可以使用原来的函数名开头,例如测试add函数,可以这样命名:函数名+should+测试结果描述,例如addShouldReturn44, addShouldThrowsException, addShouldReturn0,这样当我们测试一个类的多个函数的时候就不会看着很乱。另外,我们可能也会对于一个路径使用多个不同的值进行测试,可以用类似的方式,或者在后续会不断更新一个函数的代码,你也可以想一个比较整洁清晰的命名方式,不然的话,测试越写与多,到时你就会发现乱七八糟的名字都没耐心找了。

总结

关于软件测试中的路径覆盖测试就先到这里了,我觉得在我们平时开发中,类似这样编写单元测试基本足够了,或者你也可以横向进行输入值范围的覆盖。而写一些代码就写一个测试是十分有必要的,不然你怎么保证你的代码大部分运行都是正确的?靠想或推理吗?如果推理能保证得了,那么大部分公司就可以省掉测试这部分的支出了。

如果你想进行黑盒测试,那么也可以参考这里的路径覆盖,比白盒测试简单很多,无非就是对照需求手册,考虑输入值的范围(或对输入值进行分类),测试这个程序有没有正确实现需求上面要求的功能。

下一篇正式进入Junit5单元测试的讨论,再次学习Junit,希望能进一步提升编程技能,编程更顺利——可以续命!

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: