软件测试与验证
期中考试范围 软件缺陷 逻辑覆盖 控制流测试
全是简答题,英文试卷
二、代码单元测试
动态的代码测试:在开发环境中,通过运行被测代码,验证其是否满足期望目标,尽早发现与目标不一样的缺陷
面向代码的动态测试分为两类:代码单元测试、代码接口测试
单元测试和集成测试的区别:一个是规模,以测试执行速度的快慢来界定,不超过0.1s。另一个是独立性,单元测试不能有任何外部资源的依赖,用到了测试替身
测试替身(Test Double):替代真实代码中依赖于数据库、网络和文件系统的代码,只有形没有内容,是假的东西
单元:功能相对独立、规模较小的代码
按照软件测试技术发展规律,我们面向代码测试内容讲解分成两大部分:测试技术,测试生成
1.逻辑覆盖准则
逻辑测试:以代码中逻辑表达式结构为对象的测试,以期发现代码逻辑结构缺陷
逻辑结构缺陷:写代码时所犯错误在逻辑表达式上的可视化体现;逻辑表达式写错了,程序行为不正确
逻辑表达式缺陷类型DNF:
基于逻辑覆盖准则的测试(Logical Coverage Criteria):用于衡量代码中逻辑表达式被测试的充分程度
下图中的蓝色箭头表示A包含B,意思是B能够发现的缺陷一定可以被A发现,满足上面就一定满足下面的意思,但是判定覆盖和条件覆盖比较特殊
满足逻辑覆盖准则 ≠ 高质量测试
高质量测试是发现高质量缺陷的测试
语句覆盖Statement Coverage
用来衡量被测代码中的语句得到执行的程度,如果测试集合能够使得被测代码中的每条语句至少被执行一次,那么则说该测试集合满足了语句覆盖
语句覆盖度:
语句覆盖测试案例:
语句覆盖是定义在源代码上的,所以看的是源代码
可以在测试集合中增加多个测试用例使语句覆盖度达到100%,如例3
在逻辑测试当中,语句覆盖是最低级的,因为它只是把每个语句走了一遍,并没有去测试什么逻辑,如果把 if 里面的 && 改成 || 语句覆盖无法揭示错误
判定覆盖Decision Coverage
衡量代码中的判定得到执行的程度,如果测试集合能够使得被测代码中的每个判定至少被执行一次(指每个判定的所有可能结果都至少出现一次,例如 if((num1>1)&&(num2==0)) 的真假结果都得到执行,才认为该判定被执行),那么则说该测试集合满足了判定覆盖
条件:不含布尔算子(与或非)的逻辑表达式,例如关系表达式、布尔变量等
判定:一个或者多个条件通过一个或多个布尔算子连接起来的逻辑表达式
判定覆盖度:
判定覆盖测试案例:
注意指每个判定的真和假结果都至少出现一次,才叫做执行了
判定覆盖的缺点:主要看的是逻辑操作符用的对,逻辑操作符用的对不代表逻辑就对,不一定能发现条件缺陷,如果把 num > 1 写成了 num > -1,则不能揭示错误
注意短路操作符 && 和 ||,即当出现了false或true就不走了,MC/DC准则产生的原因,所以上个例子正确的结果表格应该是
条件覆盖 Condition Coverage
衡量代码中构成判定的各个条件得到执行的程度,如果测试集合能够使得被测代码中的每个条件至少被执行一次,那么则说该测试集合满足了条件覆盖
每个条件被执行一次的含义:每个条件的所有可能结果都至少出现一次
条件覆盖度:
条件覆盖测试用例:
notice:满足判定不一定满足条件,满足条件不一定满足判定
判定-条件覆盖 Decision-Condition Coverage(仅了解)
衡量代码中每个判定以及构成判定的每个条件得到执行的程度,如果测试集合能够使得被测代码中的每个判定至少被执行一次并且构成判定的每个条件至少被执行一次,那么则说该测试集合满足了判定-条件覆盖。
修正的判定-条件覆盖
Modified Condition/Decision Coverage,MC/DC
期望构成每个判定的每个条件能独立地影响整个判定的结果
方法:
-
使用D表示判定,ci表示D的第i个条件 -
D(ci=true)表示将D中所有ci使用true替换之后的判定表达式 -
D(ci=false)表示将D中所有ci使用false替换之后的判定表达式 -
逻辑表达式Dci= D(ci=true)⊕D(ci=false) 可以用于计算ci独立影响判定时,其它条件的测试输入值 异或算法表示两边取值不一样,所以只有Dci = true才能证明独立影响 -
注意不是每一个算法都能找到MC/DC结果
一个简单的例子:
分析:
-
第一个式子里面,c2的值没有变化,一直为真,随着c1从真变成假,结果也从真变为了假,说明c1对我们整个判定结果产生了影响 -
第二个式子里面,c1的值没有变,变的只是c2的值,c2的值变了结果也变了,c2也能够独立影响
下面是一个综合例子:
求D的mc/dc测试用例,就是把满足c1,c2,c3的测试用例结果并起来?
对于上面这个判定,使得它可以独立影响表达式结果,那么它的输入应该有3个,真真,假真,真假
注意,我们要算的是其他人取什么值才不会影响结果,使得我可以独立影响整个结果
分别求出来之后,把三种情况的测试情况求并集
下面一个实际应用:
多条件覆盖 Multiple Condition Coverage
不是对着覆盖度来去设计测试用例,而是对着功能设计,再来根据覆盖度去修改用例
2. 逻辑覆盖测试工具
两种工具:IntelliJ IDEA Code Coverage Runner/JaCoCo
IDEA Code Coverage Runner
IDEA branch coverage的计算方法与Jacoco不一样,而且似乎有缺陷
获取jacoco覆盖报告的方法
注意,想要获取覆盖率结果,工程里必须写单测代码,否则不会有结果
点击mvn test
就可以在target的site目录下生成一个 index.html文件,点开文件即可查看报告
覆盖报告分析
JaCoCo Coverage Counters
-
Instructions -
Branches 计算所有 if 和 switch 语句的分支覆盖率 无覆盖:行中没有分支被执行(红色菱形) 部分覆盖:只执行了行中的部分分支(黄色菱形) 全覆盖:行中的所有分支都已执行(绿色菱形) -
Cyclomatic Complexity 环复杂度 二值节点的个数 超过10的代码不能通过 在(线性)组合中,可以通过一种方法生成所有可能路径的最小路径数 -
Lines 语句覆盖 跟这个语句相关的二进制指令执行 当至少一条分配给该行的指令已被执行时,该行被视为已执行 无覆盖:该行中未执行任何指令(红色背景) 部分覆盖:只执行了该行中的一部分指令(黄色背景) 全覆盖:该行中的所有指令都已执行(绿色背景) -
Methods 方法覆盖 -
Classes 类覆盖
IDEA branch coverage的计算方法与Jacoco不一样,而且似乎有缺陷
3. 启发式规则
好的单元测试:r原则,自动化的,independent,可重复的repeatable
BCDE原则
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lBWx9DMW-1636267284634)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211106135205068.png)]
Heuristic Rules
没有理论基础,只是根据工程实践经验总结出来的,类似于头脑风暴
告诉你在设计测试用例时,可以遵循一个什么样的思路
Right—BICEP
-
B:边界条件(最重要) 缺陷隐藏在代码里,一般聚集在边界上,程序员在边界处经常出问题 虚假或不一致的输入值,格式错误的数据、错误的电话号码,可能导致数字溢出的计算,空值或缺失值 年龄的边界很好找,但不是所有的边界条件都那么好找,所以我们引入了correct规则寻找边界条件 -
I:逆关系 用“逆行为”测试被测试代码 在数据库中插入一条记录后,查询该记录 已经使用的款项总数 = 款项总数 – 剩余的款项数 -
C:交叉关系检查被测功能是否满足要求 使用不同数据之间的关系进行测试 已经使用的款项总数 = 款项总数 – 剩余的款项数 -
E:验错 Forcing Error Condition java中长生命周期对象引用了短生命周期对象,而短生命周期对象用完之后没有及时释放,就是内存泄露 -
P:查看性能
单元测试第一目标是要找缺陷,覆盖是顺带要达到的目标
4. Junit & Qualified 测试脚本
Junit5 Features
junit所有特性都通过allowcation(at notation?)这个功能来实现的,要记住每个at notation的含义是什么,每个功能对应哪个notation要记住,下面几个老师单独提了一下:
@test instance lifecycle 具体用法参考 TestPerClass 和 TestPerMethod 两个文件
测试类不是你的被测类,测试类是testlifecycle,被测类是meetcalendar
junit实例化测试类(instance)的方法有两种生命周期模式:
一种是在每一个测试方法之前,都会实例化一个测试类(独立性,减少测试和测试之间的依赖关系)
一种是一个测试类就实例化一个测试类的实例
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestPerClassDemo {
private int count = 0;
@Test
void addCount1(){
count++;
System.out.println("addCount1:" + count);
}
@Test
void addCount2(){
count++;
System.out.println("addCount2:" + count);
}
}
要是两个程序代码一样,只是@TestInstance的参数不同(以上面的程序为例):
第一种 MeetCalendarTestPerMethod
addcount1冒号后面是几
addcount2冒号后面是几
都是1,因为每个测试方法之前都会实例化一个testmethod实例
第二种 MeetCalendarTestPerClass
addcount1 addcount2 顺序不确定 但count是在累加的
配置junit5环境
junit需要java8以上的支持,确认java版本
在项目里新建test目录,并且标记为测试根目录
右键一个方法,generate test
窗口提示我们当前项目并没有junit的支持,点击fix,自动下载jar包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h2qyN9rn-1636267284638)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211106181613200.png)]
已经有了测试方法,至此测试环境已经准备就绪
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LD9TsFjM-1636267284639)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211106181833615.png)]
配置maven测试环境
setting maven runner jre改为1.8
project structure project sdk 也选1.8
Lifecycle
meetherejava里面有一个test lifecycle的测试类,每一个@test就是一个测试方法
例如,避免在每个方法之前都要实例化一个对象,我们可以用@beforeeach的方法进行初始化
@BeforeEach
void init() {
meet = new MeetCalendar();
}
@Test
void AddAnReservation() {
meet.addReservation("Sun1", "gymb1", "2020-09-20 18:00");
List<UserReservation> reservations = meet.getReservations();
......
}
以上代码和以下原始代码是一致的
@Test
void AddAnReservation() {
meet = new MeetCalendar();
meet.addReservation("Sun1", "gymb1", "2020-09-20 18:00");
List<UserReservation> reservations = meet.getReservations();
.......
}
下面的代码运行结果应该是
before all test, before each test, the first test, after each test, before each test, the second test, after each test, after all test
public class TestLifeCycle {
@BeforeAll
public static void initAll(){
System.out.println("Before all tests");
}
@BeforeEach
void init(){
System.out.println("Before each test");
}
@Test
void testDemoMethod1(){
System.out.println("The 1st test");
}
@Test
void testDemoMethod2(){
System.out.println("The 2nd test");
}
@AfterEach
void tearDown(){
System.out.println("After each test");
}
@AfterAll
static void tearAll(){
System.out.println("After all tests");
}
}
Assertions
assertAll():
分组不同的断言,所有断言是被执行,任何失败都会一起报告
assertEquals("Sun", inputReservation.getUserName());
assertEquals(Site.gymb1, inputReservation.getSite());
assertEquals("2019-10-28 18:00",
inputReservation.getReservationDateTime().format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
MeetHereMaven 项目中 MeetCalendarTest 的 addAnReservation() 中的以上代码,可以用assertAll()改写为
assertAll(
() -> assertEquals("Sun", inputReservation.getUserName()),
() -> assertEquals(Site.gymb1, inputReservation.getSite()),
() -> assertEquals("2020-9-20 18:00",
inputReservation.getReservationDateTime().format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))));
assertThrows():
系统抛出期望的异常
注意两点,一点系统是否抛出了期望的异常类型,一点处理异常的行为是不是正确
MeetHereMaven 项目中 DateTimeConvertTest 中有异常处理的例子
第一个参数是抛出的异常类型,第二个参数是我们输入的拉姆达表达式
@Test
@DisplayName("转换无效格式的日期,系统抛出异常")
void convertInvalidDateString(){
Throwable exception = assertThrows(RuntimeException.class,()->DateTimeConvert.convertStringToDateTime("2019-9-20 18:00"));
assertEquals("输入预约时间格式不正确: [2019-9-20 18:00], 输入时间格式为[yyyy-MM-ddHH:mm]",exception.getMessage());
}
Parameterized tests 参数化测试
@ParameterizedTest 与 @Test 相同的生命周期
这是Junit5中实现的数据驱动自动化测试技术,通俗的讲就是只写一个测试方法,可以跑不同的数据
将测试数据和测试行为进行了分离,测试数据源和数据脚本是分开的,数据不是写在代码里
使用不同的参数多次运行测试,一段代码可以执行很多测试数据
学参数化测试要学哪些东西至少三点,最重要的是测试数据源,一是为测试定义测试数据类型,二是参数转换,测试数据源如何处理变量类型,类型匹配,类型转化,三是自己的属性
一是 定义数据源
参数指的的测试方法里面的方法的参数,测试数据源是这一个集合,每一条叫一个测试数据argument
测试数据源(argument source 简称 AS)的基本使用原则:
AS规则1:每个@Parameterized测试方法可以使用多个测试数据源,但是至少需要有一个
@ParameterizedTest
@ValueSource(strings = {"software testing","Junit5","Have fun!"})
void lowerCase(String candidate) {
assertTrue(StringUtils.isAllLowerCase(candidate));
}
@ParameterizedTest
void testMethodWithoutArgumentSource(int candidate) {
assertEquals(9,candidate);
}
下面代码给了两个数据源,用了junit5提供的两种定义数据源的方式,一种是一维数组@ValueSource,一种是使用方法来指定数据源@MethodSource,这个方法的名字叫range,但是必须返回一个string兼容类型
下面这个方法会运行8次,range是包含0,不包含20,但是从第15个往后跳,只有15 16 17 18 19,运行5次
@ParameterizedTest
@ValueSource(ints = {1,2,3})
@MethodSource("range")
void testMethodWithMultipleArgumentSource(int candidate) {
assertNotEquals(9,candidate);
}
AS规则2:每个测试数据源必须为测试方法的所有参数提供测试数据。例如,测试方法若有2个参数,参数1不能使用测试数据源1,而参数2使用其他数据源,即参数2也必须使用测试数据源1
比如下面这个程序是不对的会报错
@ParameterizedTest
@ValueSource(ints = {1,2,3})
@MethodSource("range")
void testMethodWithMultipleArgumentSource(int candidate1,int candidate2) {
assertNotEquals(9,candidate1);
assertNotEquals(-1,candidate2);
}
常用的定义数据源的七种方法:
@ValueSource 一维数组
@NullSource, @EmptySource, @NullAndEmptySource 测试Null and Empty Sources:
@EnumSource 枚举数据源类型
@MethodSource
@CsvSource 逗号分割数据源,用的最多,excel可以生成csv,也有公司放在数据库里
@CsvFileSource
@ArgumentsSource
csv定义数据源方法举例
@ParameterizedTest
@CsvSource({"apple,1","orange,2","'lemon,lime',0xF1"})
void testWithCsvSource(String fruit,int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@ParameterizedTest
@CsvSource(delimiter= ';',value = {"apple;1","orange;2","'lemon,lime';0xF1"})
void testWithCsvSourcebyAtt(String fruit,int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
求出基路径:找到被测代码中必须要执行的测试路径
收尾相连的边构成的节点序列
不是程序的完整通路
complete path 完整路径:这条路径的开始必须是开始节点,终止必须是终止节点
|