软件测试与验证
文章目录
二、代码单元测试
动态的代码测试:在开发环境中,通过运行被测代码,验证其是否满足期望目标,尽早发现与目标不一样的缺陷
面向代码的动态测试分为两类:代码单元测试、代码接口测试
单元测试和集成测试的区别:一个是规模,以测试执行速度的快慢来界定,不超过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原则
Heuristic Rules
没有理论基础,只是根据工程实践经验总结出来的,类似于头脑风暴
告诉你在设计测试用例时,可以遵循一个什么样的思路
Right—BICEP
-
Right
找到happy path 就是满足下面所有条件的测试用例
-
B:边界条件(最重要)
缺陷隐藏在代码里,一般聚集在边界上,程序员在边界处经常出问题
虚假或不一致的输入值,格式错误的数据、错误的电话号码,可能导致数字溢出的计算,空值或缺失值
年龄的边界很好找,但不是所有的边界条件都那么好找,所以我们引入了correct规则寻找边界条件
-
I:逆关系
用“逆行为”测试被测试代码
在数据库中插入一条记录后,查询该记录
已经使用的款项总数 = 款项总数 – 剩余的款项数
-
C:交叉关系检查被测功能是否满足要求
使用不同数据之间的关系进行测试
已经使用的款项总数 = 款项总数 – 剩余的款项数
-
E:验错 Forcing Error Condition
java中长生命周期对象引用了短生命周期对象,而短生命周期对象用完之后没有及时释放,就是内存泄露
-
P:查看性能
单元测试第一目标是要找缺陷,覆盖是顺带要达到的目标
4. Junit 测试脚本
Junit5 Features
junit所有特性都通过allowcation(at notation?)这个功能来实现的,要记住每个at notation的含义是什么,每个功能对应哪个notation要记住,下面几个老师单独提了一下:
-
@displayname的作用,提高可读性
-
@disable 忽略执行
-
@test instance lifecycle ⭐️
@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-ITdT3RxD-1640020102917)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211106181613200.png)]
已经有了测试方法,至此测试环境已经准备就绪
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u6aAUZNs-1640020102917)(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(){
// 1. 测试系统是否抛出了期望的异常类型
Throwable exception = assertThrows(RuntimeException.class,()->DateTimeConvert.convertStringToDateTime("2019-9-20 18:00"));
// 2. 处理异常的行为是不是正确
assertEquals("输入预约时间格式不正确: [2019-9-20 18:00], 输入时间格式为[yyyy-MM-ddHH:mm]",exception.getMessage());
}
Parameterized tests 参数化测试
@ParameterizedTest 与 @Test 相同的生命周期
这是Junit5中实现的数据驱动自动化测试技术,通俗的讲就是只写一个测试方法,可以跑不同的数据
将测试数据和测试行为进行了分离,测试数据源和数据脚本是分开的,数据不是写在代码里
使用不同的参数多次运行测试,一段代码可以执行很多测试数据
学参数化测试要学哪些东西至少三点,最重要的是测试数据源,一是为测试定义测试数据类型,二是参数转换,测试数据源如何处理变量类型,类型匹配,类型转化,三是自己的属性
(1)定义数据源
参数指的的测试方法里面的方法的参数,测试数据源是这一个集合,每一条叫一个测试数据argument
(1).1 测试数据源(argument source 简称 AS)的基本使用原则:
AS规则1:
每个@Parameterized测试方法可以使用多个测试数据源,但是至少需要有一个
@ParameterizedTest
@ValueSource(strings = {"software testing","Junit5","Have fun!"})
// valuesource用一维数组的方式来定义参数化测试的数据源
void lowerCase(String candidate) {
assertTrue(StringUtils.isAllLowerCase(candidate));
}
@ParameterizedTest
void testMethodWithoutArgumentSource(int candidate) {
assertEquals(9,candidate);
}
// 第二个没有数据源会报错,org.junit.platform.commons.PreconditionViolationException: Configuration error: You must configure at least one set of arguments for this @ParameterizedTest
下面代码给了两个数据源,用了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);
}
(1).2 常用的定义数据源的七种方法:
详见junitDemo项目
@ValueSource 一维数组
@NullSource, @EmptySource, @NullAndEmptySource 测试Null and Empty Sources:
@EnumSource 枚举数据源类型
@MethodSource
@CsvSource 逗号分割数据源,用的最多,excel可以生成csv,也有公司放在数据库里
@CsvFileSource
@ArgumentsSource
-
@EnumSource
有两个可选的属性,用哪一个枚举量,跟模式是相关的,INCLUDE, EXCLUDE, MATCH_ALL,MATCH_ANY
//1 must attribute value: @EnumSource(TimeUnit.class) //2 Optional attributes name: @EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" }) mode: @EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
-
@CsvSource
@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); }
-
@MethodSource
参考一种或多种方法(工厂方法)为测试方法提供测试数据
这个制造数据的方法必须是static,不能带参数
如果@MethodSource 中没有给出工厂方法,则选择与测试同名的方法作为工厂方法
//字符串 @ParameterizedTest @MethodSource({"stringArguments","stringArgumentsWithStream","stringArray"}) void testWithString(String argument) { assertNotEquals("hysun", argument); } static List<String> stringArguments(){ return Arrays.asList("software","testing"); } static Stream<String> stringArgumentsWithStream(){ return Stream.of("Junit","Mockito"); } static String[] stringArray(){ String[] str_data = {"selenium","appnium"}; return str_data; } //单个数字 @ParameterizedTest @MethodSource("singleInteger") void testSingleGrade(int grade){ assertTrue(grade >= 0 && grade <= 100); } static int singleInteger(){ return 85; } //list //测试方法有多个参数,工厂方法返回 Stream、Iterable、Iterator 或 Arguments 类型的数组 @ParameterizedTest @MethodSource("provideWithList") void testMultipleGrade(int grade1,int grade2){ assertTrue(grade1 >= 0 && grade1 <= 100); assertTrue(grade2 >= 0 && grade2 <= 100); } static List<Arguments> provideWithList(){ return Arrays.asList(Arguments.of(90,45), Arguments.of(60,30), Arguments.of(85,42)); }
工厂方法可以是测试类中的方法,也可以是其他类的方法
至于来自外部类的工厂方法,需要完整的类名,不要忘记包名!! -
@CsvFileSource
通过引用resource里面的文件
如果把lemon,line的双引号改成了单引号
则下面的代码会报错
如果想要提取excel文件并跳过表头,numLinesToSkip = 1
-
@ArgumentsSource
通过实现 ArgumentProvider 接口自定义参数源
下面定义一个类实现接口
public class GradeArgumentsProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext var1) { return IntStream.of(90,60,85) .mapToObj( grade-> Arguments.of(grade,0.5*grade) ); } }
然后可以在另一个类里使用 ,ArgumentsSource参数是实现了接口的类
@ParameterizedTest @ArgumentsSource(GradeArgumentsProvider.class ) void testFinalGrateGradeWithArgumentProvider(int grade, double finalGrade){ assertEquals(2.0, grade/finalGrade,0.0001); }
(2)参数类型转换
显示转换
实现 ArgumentConverter 接口/扩展 SimpleArgumentConverter
SimpleArgumentCoverter 是一个抽象类实现了 ArgumentCoverter接口
我们可以直接定义一个类继承 SimpleArgumentCoverter
在参数中@ConvertWith
隐式转换
Junit5 内置隐式类型转换器:字符串转换为参数类型
回退字符串到对象的转换Fallback String-to-Object Conversion:工厂方法和工厂构造函数
(3)给测试命名
@ParameterizedTest(name=“[{index}] {arguments}”)
每一次测试的名字,提高可读性
index第几次执行,后面argument是把所有参数值都列出来
举例1
举例2
生命周期方法不可以参数化,但可以在生命周期测试方法中去获取信息,提高可读性:用到两个类TestInfo和TestReporter
5. mockito
meetheremaven中有一个类叫 add
基于行为的测试,不是基于状态的测试
基于行为需要mokito这样一个框架帮助我们,doc verify
设计脚本的三个阶段arrange action assert
6. 基于控制流的测试
recording4 7:14 开始的
前面用了单元测试主要用的技术和方法,现在要学测试用例自动生成方法
测试理论的三大支柱:基于控制流的(Control Based Tests Generation),基于数据流的(Data Flow Based Testing),基于遍历的(Mutation Testing)
基于控制流主要介绍基路径测试:基于程序控制结构设计可覆盖基本执行路径的测试的测试设计技术
- 构建控制流图(CFG)
- 计算基路径集,两个算法复杂度都不低
- 推出/计算测试用例
6.1 构建控制流图
控制流图是对代码中所有执行的一个抽象,用一张图表达出来
两个元素,一个是节点,一个是边,符合离散数学中图论的定义
节点是指代语句或一系列顺序执行的语句,如果第一个语句被执行,所有语句都将被执行
边指代代码当中控制的转移,是对指令顺序的一个抽象
if语句
switch语句:分支可以有好多个
覆盖度工具中有个环复杂度,计算的是二值节点,switch就不是二值节点
while语句和do-while语句
for语句
一个例子,注意红色箭头容易出错的部分
学会如何去切代码!
切代码exercise:
6.2 计算基路径
基于控制流测试的第二步:计算基路径
求出基路径:找到被测代码中必须要执行的测试路径
路径:首尾相连的边构成的节点序列
路径的长度:路径中包含的边的数量,允许长度为0的路径就是一个节点,一条语句就是一个节点
子路径:节点子序列构成的路径
根据控制流图,可以得到相应的路径,从长度为1到长度为10的路径都有,这里的路径不是程序的完整路径,只是节点的序列
而基路径是要在成千上万的路径中找出比较有代表性的路径集合,因为资源有限不可能每一条路径都去测试
complete path完整路径:这条路径的开始必须是开始节点,终止必须是终止节点
Infeasible Complete Path不可达路径:水浒传第134位?
simple path简单路径:是一种特殊的路径,除了第一个节点和最后一个节点外,中间节点出现的次数有且只有一次。(目的是剔除中间包含循环的那些路径)
怎样去处理这么庞大数量的路径呢,基路径帮助我们减少要去测试的路径
prime path基路径
不作为任何其他简单路径的正确子路径的简单路径,基路径不一定是一条完整路径
所以上一道题只有长度为3的简单路径是基路径
控制流图的所有路径都可以通过基路径的加或者乘得出,跟向量中的"基"含义一样
How to calculate prime path
- Exhaust method (穷举)
- Node tree method (节点树法,同学想出来的)
学期作业可以构造复杂度更低的方法来完成
(1)暴力法step
- 从len 0 开始列出所有的简单路径
- 将列出来的不能再扩展的路径标记感叹号
- 继续扩展列出 len 1 len 2 … len 4,直到所有的集合都被标记上感叹号
- 长度最长的一定是基路径
- 再找出其他长度(从高到低找)里不是别人子路径的简单路径
(2)节点树法 step
方法介绍:
-
节点树
以G中的节点为根节点建立的树,且满足树中除根节点和叶节点 可以相同外,从根节点到每个树中节点的路径中,每个节点的出现次数有且仅有 1 次。在节点树中,每条从根节点到叶节点的路径即为一条简单路径
-
简单节点树
若节点树T不是任何其它节点树的子树,则称节点树T为简单节点树
所有简单节点树的从根节点到叶节点的路径集合为备选的基路径 集合
步骤
- 画出每个结点的节点树
- 0节点数都是基路径,再一个一个找没有被0节点数的路径包含的基路径
6.3 自动生成测试集合
“ Deriving Test Cases ”
测试生成算法目标:Prime Path Coverage (PPC) 每条基路径应至少被一个测试用例执行一次
步骤:
- 把基路径扩展成完整路径:从最长的基路径开始,将其扩展成包含CFG的初始节点和终止节点的完全路径
- 计算使得路径能够被执行的输入作为测试输入
方法:
-
观察法
-
约束求解法:将路径构造为约束表达式,再用工具求解
当构成路径表达式的各个变量的取值使得路径表达式的结果为真,说明在该取值下,程序执行该条路径
我们这个课约束求解器用的是Z3
生成工具:把代码通过soot编译成中间形式,利用soot提供的生成控制流图工具构造被测源码的控制流图,第二步找基路径,他这里没有写基路径,只用了soot里面直接提供有个getExtendedBasicBlockPathBetween()方法,直接提供了从开始节点到终止节点的路径,这里没有写基路径,这是交给大家学期作业去写的,测下来对循环不大友好会出错
demo
假设根据控制流算出了两条路径,接下来就要去找约束表达式:
不再用 1→2 表示路径,而是用 op1>=op2 来表示这条路径
三指令表达式 smp
工具针对的是方法,给定任意一个java方法,可以可视化控制流图
-
graphic里面是基于soot框架,去解析被测的测试方法,soot里面的unitgraph类去构造控制流图ug
static方法getMethodCFG()里面传入的参数,第一个是类的路径,第二个参数是类名,第三个是方法名
-
visualizer
printCFGDot(),用dot画出控制流图,第三个参数indexLabel表示dot中显示节点序号(true),还是Soot代码(false)
getLabelFromUnit(Unit unit) 这个方法需要扩展,先调用sootCFG去生成控制流图得到这样一个类,然后再把这个类printCFGDot,
-
test -> visualizer
multiple_if_cfgDot() 其实是把soot里面的getCFG()方法拷贝过去的
@Test void multiple_if_cfgDot() { //78-98行代码都是 soot编译生成控制流图 //大概的意思就是把用jimple编译过的方法的方法体传递给soot里面的类 String sourceDirectory = System.getProperty("user.dir") + File.separator + "target" + File.separator + "test-classes"; System.out.println(sourceDirectory); String clsName = "cut.LogicStructure"; String methodName = "multipleIf"; //setupSoot System.out.println(sourceDirectory); G.reset(); Options.v().set_prepend_classpath(true); Options.v().set_allow_phantom_refs(true); Options.v().set_soot_classpath(sourceDirectory); SootClass sc = Scene.v().loadClassAndSupport(clsName); sc.setApplicationClass(); Scene.v().loadNecessaryClasses(); // Retrieve method's body SootClass mainClass = Scene.v().getSootClass(clsName); SootMethod sm = mainClass.getMethodByName(methodName); JimpleBody body = (JimpleBody) sm.retrieveActiveBody(); //把数据流图画出来 UnitGraph ug = new ClassicCompleteUnitGraph(sm.retrieveActiveBody()); Visualizer.printCFGDot("multipleIf_cfg_label_with_index",ug,true); Visualizer.printCFGDot("multipleIf_cfg_label_with_SootCode",ug,false); }
@变量名是soot自己加的变量,不是我们源代码中的变量
我们的工作就是要把控制流图中的信息提取出来,构造路径表达式,放到z3里去约束求解
-
test -> cut 被测代码 class and test 的缩写?
是我们这个工具的测试数据,是一段段程序,不是一个个数据
测试数据LogicStructure里面的multipleIf()就是我们要测试的方法
-
SimpleGenerator 和 solver 是核心 一个是求解,一个是生成测试用例
-
SimpleGenerator
两个构造函数
public SimpleGenerator(String className, String methodName) { String defaultClsPath = System.getProperty("user.dir") + File.separator + "target" + File.separator + "classes"; new SimpleGenerator(defaultClsPath, className, methodName); } public SimpleGenerator(String classPath, String className, String methodName) { clsPath = classPath; clsName = className; mtdName = methodName; ug = SootCFG.getMethodCFG(clsPath, clsName, mtdName); body = SootCFG.getMethodBody(clsPath, clsName, mtdName); }
核心生成代码
public List<String> generate() { //核心代码 生成测试用例的地方 List<Unit> path = null; ArrayList<String> testSet = null; String pathConstraint = ""; System.out.println("============================================================================"); System.out.println("Generating test case inputs for method: " + clsName + "." + mtdName + "()"); System.out.println("============================================================================"); try { testSet = new ArrayList<String>(); for (Unit h : ug.getHeads()) //76-79行在找路径 找soot提供的从开始到终止的基本路径 找的不是基路径 for (Unit t : ug.getTails()) { path = ug.getExtendedBasicBlockPathBetween(h, t); System.out.println("The path is: " + path.toString()); //path取出来是unit序列 soot语句 pathConstraint = calPathConstraint(path); //判断它是不是分支路径 计算约束表达式 //如果路径约束为空字符串,表示路径约束为恒真 //如果是分支路径,对路径进行求解,如果是true就开始随机生成 if (pathConstraint.isEmpty()) testSet.add(randomTC(body.getParameterLocals())); System.out.println("The corresponding path constraint is: " + pathConstraint); if (!pathConstraint.isEmpty()) testSet.add(solve(pathConstraint)); } } catch (Exception e) { System.err.println("Error in generating test cases: "); System.err.println(e.toString()); } if (!testSet.isEmpty()) { System.out.println(""); System.out.println("The generated test case inputs:"); int count = 1; for (String tc : testSet) { System.out.println("( " + count + " ) " + tc.toString()); count++; } } return testSet; }
计算路径函数
大概逻辑是,我现在已经获得了一条路径信息,当我要去计算约束的时候,如果有if判断语句,我就要去判断是不是我当前这条语句的下面一条语句,如果是说明if条件取得是真,如果不一样说明if条件取得是假
然后发现它的变量名是jimple生成的变量,不是我们真正的变量,所以还要做一次替换
public String calPathConstraint(List<Unit> path) { List<Local> jVars = getJVars(); String pathConstraint = ""; String expectedResult = ""; HashMap<String, String> assignList = new HashMap<>(); ArrayList<String> stepConditionsWithJimpleVars = new ArrayList<String>(); ArrayList<String> stepConditions = new ArrayList<String>(); for (Unit stmt : path) { if (stmt instanceof JAssignStmt) { assignList.put(((JAssignStmt) stmt).getLeftOp().toString(), ((JAssignStmt) stmt).getRightOp().toString()); continue; } if (stmt instanceof JIfStmt) { String ifstms = ((JIfStmt) stmt).getCondition().toString(); int nextUnitIndex = path.indexOf(stmt) + 1; Unit nextUnit = path.get(nextUnitIndex); //如果ifstmt的后继语句不是ifstmt中goto语句,说明ifstmt中的条件为假 if (!((JIfStmt) stmt).getTarget().equals(nextUnit)) ifstms = "!( " + ifstms + " )"; else ifstms = "( " + ifstms + " )"; stepConditionsWithJimpleVars.add(ifstms); continue; } if (stmt instanceof JReturnStmt) { expectedResult = stmt.toString().replace("return", "").trim(); } } System.out.println("The step conditions with JimpleVars are: " + stepConditionsWithJimpleVars); //bug 没有考虑jVars为空的情况 if (jVars.size() != 0) { for (String cond : stepConditionsWithJimpleVars) { //替换条件里的Jimple变量 for (Local lv : jVars) { if (cond.contains(lv.toString())) { stepConditions.add(cond.replace(lv.toString(), assignList.get(lv.toString()).trim())); } } } } else stepConditions = stepConditionsWithJimpleVars; if (stepConditions.isEmpty()) return ""; pathConstraint = stepConditions.get(0); int i = 1; while (i < stepConditions.size()) { pathConstraint = pathConstraint + " && " + stepConditions.get(i); i++; } //System.out.println("The path expression is: " + pathConstraint); return pathConstraint; }
运行
soot提供的jimple
public class SootCFG {
public static UnitGraph getMethodCFG(String sourceDirectory,String clsName,String methodName){
Body body = getMethodBody(sourceDirectory,clsName,methodName);
UnitGraph ug = new ClassicCompleteUnitGraph(body);
return ug;
}
public static Body getMethodBody(String sourceDirectory,String clsName,String methodName){
G.reset();
Options.v().set_prepend_classpath(true);
Options.v().set_allow_phantom_refs(true);
Options.v().set_soot_classpath(sourceDirectory);
SootClass sc = Scene.v().loadClassAndSupport(clsName);
sc.setApplicationClass();
Scene.v().loadNecessaryClasses();
SootClass mainClass = Scene.v().getSootClass(clsName);
SootMethod sm = mainClass.getMethodByName(methodName);
Body body = sm.retrieveActiveBody();
return body;
}
}
基路径生成的测试集合不够,其他测试方法自动生成测试用例
节点覆盖 node
边覆盖 edge
边对覆盖 长度为2的所有路径都要测量
全对偶测试
测试揭错能力图,灰色的部分是数据流图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DpSbGYxQ-1640020102928)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211212215121993.png)]
7.基于数据流的测试
控制流考虑的是代码中的分支机构,数据流考虑对变量进行定义和使用的测试
本章包含的问题:如何构造数据流图、怎样基于数据流图找数据输入、哪些数据流覆盖准则
数据流不是测试当中特有的概念,是编译原理里面的
跟数据流相关的概念:
-
变量的定义def:将变量的值存入内存的语句,对变量的写操作 x=44 input
-
变量的使用use:读取或访问变量的值的语句,不改变变量的值,比如条件语句
-
数据流测试的关键就是,检测变量的定义有没有用,是不是该用,是不是正确
-
有些语句既读又写
-
区别c-use和p-use
exercise 1
注意6不是p的定义节点
程序缺陷在于没有回收内存空间
exercise 2
数据流图
控制流测试设计的思想:执行程序路径到达期望的控制覆盖准则
数据流测试设计的思想:执行程序路径到达期望的数据流覆盖准则
du pair 定义使用对:一个二元组 (li,lj),在li处定义,在lj处使用
def clear定义清除路径:从li开始,在lg结束,如果变量是定义清除的,那么除了 li 以外其他的节点都是不是定义节点
⭐️du path 定义使用路径:首先是简单路径,必须是定义清除的
定义使用路径的两种表示方式,第二个集合应该等于第一个集合的并集
例题
数据流覆盖准则
全定义覆盖 ADC:对每个变量v而言,每个定义必须被至少执行一次
全使用覆盖 AUC:对每个变量而言,所有的使用必须在测试用例中出现一次
全定义使用路径覆盖 ADUPC:对于每个变量而言,执行所有的定义使用路径
exercise:
第一步画出控制流图
第二步画出数据流图
第三步
列出定义使用对
以2为开头的
以10开头的,注意从10到10也是一个路径
第四步
选出全定义覆盖
第五步
选出全使用覆盖路径
注意绿色的部分挑一个出来就像
第六步
选出全定义使用覆盖路径
8. 变异测试
基本概念
企业界的理解就是做出来就行
学理届一定要系统的用科学的方法和态度把它做出来
变异测试是揭错能力最强的方法,在代码当中故意植入一些缺陷,但是在工程界不被广泛应用
他开始是用来评估测试揭错能力的标准,后来才演变成了测试生成方法
变异算子:
缺陷,用于引入错误的方法
对被测代码做语法上的改变,这种改变的途径就是变异算子,一般是一个集合,每个变异算子代表了一类缺陷
每用一个变异算子,就生成一个变异体mutant,一次只改变一个地方(可以是变量交换,可以是改变符号)
一阶变异:Only one change
高阶变异:More than one changes
变异得分:
客观的数量指标
计算方法:被杀死的变异体 / (被杀死的变异体+存活的变异体)
分母剔除了等价变异体,就是运行结果跟以前完全一样的
NE表示只要前面有一个不一样,后面就不用算了
基于变异的测试生成
rip模型
可达性:测试导致错误语句被到达(在变异中——变异语句)
感染:测试导致错误的语句导致错误的状态
传播:不正确的状态传播到不正确的输出
RIP 模型导致突变覆盖的两种变体(强杀和弱杀)
Mutation Coverage (MC变异覆盖) :杀死所有的变异体,运行出来结果全都不一样,要满足rip模型
基于强变异覆盖
强杀:生成用例是通过输出结果来判断变异体是不是被杀死,有可观测的结果,要求非常高
基于弱变异覆盖
弱杀:只用满足可达性和感染性就行了, 不需要满足传播性了,条件放宽了
等价变异体
也可以通过约束求解表达式来分析
源代码的等价,然而前面的语句是“minVal = A” 替换,我们得到:“(B < A) != (B < A)”,因此没有输入可以杀死这个突变体
exercise
变异算子设计
程序每一句话都可以构造出一个变异算子,问题是构造这个变异算子有没有价值
如果专门为杀死由一组变异算子 O = {o1, o2, …} 创建的突变体而创建的测试也以非常高的概率杀死由所有剩余变异算子创建的突变体,则 O 定义了一组有效的变异算子
插入一元运算符和修改一元和二元运算符都有效