我们希望每段代码都是自测试的,每次改动之后,都能自动发现对现有功能的影响。
1 测试要求
在对软件单元进行动态测试之前,应对软件单元的源代码进行静态测试;
应建立测试软件单元的环境,如数据准备、桩模块、模拟器。
对软件设计文档规定的软件单元的功能、性能、接口等应逐项进行测试。
每个软件特性应至少被一个正常测试用例和一个被认可的异常测试用例覆盖
测试用例的输入应至少包括 有效等价类值、无效等价类值 和 边界数据值;
语句覆盖率要达到要求(如70%);
分支覆盖率要达到要求(如70%);
对输出数据正确与否 及 其格式进行测试。
2 单元测试任务
模块接口测试;
模块局部数据结构测试;
模块边界条件测试;
模块中独立执行路径测试;
模块的错误处理路径测试。
3 静态分析
控制流分析:根据设计文档定义的控制流程,分析被测试程序是否按要求运行。
数据流分析:用控制流程图来分析数据发生的异常情况,这些异常包括被初始化、被赋值或被引用过程中行为序列的异常。数据流分析也作为数据流测试的预处理过程
推荐使用“TDD测试驱动开发”的方式,开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么代码产品,之后再开始真正的业务需求开发。
4 动态测试
4.1 功能测试
功能测试是对软件设计中的软件单元逐项进行的测试,以验证其功能是否满足要求。
功能测试一般需进行:
用正常值的等价类输入数据值测试;
用非正常值的等价类输入数据值测试;
进行每个功能的合法边界值和非法边界值输入的测试;
用一系列真实的数据类型和数据值运行,测试超负荷、饱和及其他“最坏情况”的结果
4.2 接口测试
接口测试是对软件设计文档中的外部/内部接口逐项进行的测试。
接口测试一般需进行:
测试所有外部/内部接口,检查接口信息的格式和内容;
对每一个外部/内部输入/输出接口必须进行正常和异常情况的测试
4.3 边界测试
边界测试是对软件处在边界或端点情况下运行状态的测试。
边界测试一般需进行:
软件的输入域或输出域的边界或端点的测试;
状态转换的边界或端点的测试;
功能界限的边界或端点的测试;
性能界限的边界或端点的测试;
容量界限的边界或端点的测试。
4.4 逻辑测试
逻辑测试是测试程序逻辑结构的合理性、实现的正确性。逻辑测试应由测试人员利用程序内部的逻辑结构及有关信息,设计或选择测试用例,对程序所有逻辑路径进行测试。通过在不同点检查程序的状态,确定实际的状态是否与预期的状态一致。
逻辑测试应满足覆盖率的要求,一般需进行:
- 语句覆盖;
- 分支覆盖;
- 条件覆盖;
- 条件组合覆盖;
- 路径覆盖。
5 单元测试用例设计方法
等价类划分、边界值分析、逻辑覆盖法、基本路径测试法
5.1 等价类划分
等价类划分的方法是把程序的输入划分成若干等价类部分,然后从每个部分中选取少数代表性数据当作测试用例。
确定等价类原则:
- 如果输入变量定义了一个取值范围(例如正常取值为[1,999]),那么就应确定一个有效等价类(1≤输入≤999),以及两个无效等价类(输入<1,输入>999);
- 如果输入变量为枚举类型,或对于规定的有限输入值集合分别进行不同的处理,那么应对每一个输入确定一个有效等价类,并确定一个无效等价类;
- 如果对输入变量进行逻辑判断,那么应对逻辑条件满足与否分别确定一个有效等价类和一个无效等价类;
- 如果输入变量作为数组下标或指针使用,那么应根据规定的数组元素个数,分别确定有效等价类和无效等价类。
5.2 边界值分析
- 如果规定了变量的输入范围,那么应该对范围的边界设计测试用例;对于未具体规定取值范围的输入变量,应考虑对变量数据类型所能达到的最大范围的上下限设计测试用例;
- 如果规定了数组、循环逻辑次数等数量,应针对最小、最大数量值的边界设计测试用例;
- 如果规定了输出变量的范围,还应考虑针对输出变量范围边界所对应的输入变量数值设计测试用例;
- 对各边界条件的上点和离点作为测试输入数据
- 如果输入条件规定了值的个数,则用最大个数,最小个数,比最小个数少一,比最大个数多一的数作为测试数据
- 如果程序的规格说明给出的输入域或输出域是有序集合,则应选取集合的第一个元素和最后一个元素作为测试用例
5.3 逻辑覆盖法
逻辑覆盖法是根据程序内部逻辑覆盖满足性设计测试用例的白盒测试方法。
(1)语句覆盖:就是设计若干个测试用例,运行被测程序,使得每一可执行语句至少执行一次。
(2)分支覆盖:(判定覆盖)使设计的测试用例保证程序中每个判断的每个取值分支至少经历一次。但若程序中的判定是有几个条件联合构成时,它未必能发现每个条件的错误;
(3)条件覆盖:条件覆盖是指选择足够的测试用例,使得运行这些测试用例时,判定中每个条件的所有可能结果至少出现一次,但未必能覆盖全部分支;
(4)分支 / 条件覆盖:分支/条件覆盖是使判定中每个条件的所有可能结果至少出现一次,并且每个分支本身的所有可能结果也至少出现一次;
(5)条件组合覆盖:条件组合覆盖是使每个分支中条件结果的所有可能组合至少出现一次,因此分支本身的所有可能结果也至少出现一次
(6)路径覆盖:是每条可能执行到的路径至少执行一次。
补充:
(1)语句覆盖在所有的测试方法中是一种最弱的覆盖。
(2)判定覆盖和条件覆盖比语句覆盖强,满足判定/条件覆盖标准的测试用例一定也满足判定覆盖、条件覆盖和语句覆盖
(3)路径覆盖也是一种比较强的覆盖,但未必考虑判定条件结果的组合,并不能代替条件覆盖和条件组合覆盖。
举个例子吧:
if A and B then Action1
if C or D then Action2
语句覆盖最弱,只需要让程序中的语句都执行一遍即可 。上例中只需设计测试用例使得A=true B=true C=true 即可。
分支覆盖又称判定覆盖:使得程序中每个判断的取真分支和取假分支至少经历一次,即判断的真假均曾被满足。上例需要设计测试用例使其分别满足下列条件即可
(1)A=true,B=true,C=true,D=false
(2)A=true,B=false,C=false,D=false。
条件覆盖:要使得每个判断中的每个条件的可能取值至少满足一次。上例中第一个判断应考虑到A=true,A=false,B=true,B=false第二个判断应考虑到C=true,C=false,D=true,D=false,所以上例中可以设计测试用例满足下列条件
(1)A=true,B=true,C=true,D=true
(2)A=false,B=false,C=false,D=false。
路径覆盖:要求覆盖程序中所有可能的路径。所以可以设计测试用例满足下列条件
(1)A=true,B=true,C=true,D=true
(2)A=false,B=false,C=false,D=false
(3)A=true,B=true,C=false,D=false
(4)A=false,B=false,C=true,D=true。
不论那种覆盖方法,都不能保证程序的正确性。
一般测试书中讲白盒测试的逻辑覆盖部分时都会对这几种覆盖作比较,而且都给出了例子。
我们要求的是分支覆盖。
5.4 基本路径测试
上图中,独立路径有5条,即需要有5个测试用例覆盖。
我们会统计独立路径的覆盖率。
代码规范中对方法复杂度有要求,一个方法的复杂度太高,容易出错且难以测试。
6 单元测试的实现方法
6.1 mock
单元测试不是集成测试,只测试自身的逻辑;因此,我们使用mockito库,模拟返回外部接口或者其它函数的结果
6.3 数据准备
使用@Sql
7 单元测试与集成测试
写单元测试时,容易有一个误区,就是都写的集成测试,而没有对某个方法写单元测试,这其实是一种想要偷懒但其实低效的方法,包括编写、运行、效果都是低效的。
对每个层进行特定的测试方法,会更新高效。
单元测试与集成测试的比例大概是3:1。使用单元测试去覆盖分支,效率会更高。
使用gradle 的JVM Test Suite Plugin划分单元测试与集成测试集,分开执行,单元测试开启gradle的并行运行模式。
7.1 controller层
这层主要,测试参数校验。
需要使用“@WebMvcTest”注解。
mock掉service
@WebMvcTest(EmployeeController.class)
public class StandaloneControllerTests {
@MockBean
EmployeeService employeeService;
@Autowired
MockMvc mockMvc;
@Test
public void testfindAll() throws Exception {
Employee employee = new Employee("Lokesh", "Gupta");
List<Employee> employees = Arrays.asList(employee);
Mockito.when(employeeService.findAll()).thenReturn(employees);
mockMvc.perform(get("/employee"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.hasSize(1)))
.andExpect(jsonPath("$[0].firstName", Matchers.is("Lokesh")));
}
}
7.2 service层
主要的业务逻辑层,需要对各个业务分支代码进行测试,是单元测试最多的层。
使用mock,不需要使用“@SpringBootTest”注解,运行和编写的效率更高。
@ExtendWith(MockitoExtension.class)
public class ServiceTests {
@InjectMocks
EmployeeService service;
@Mock
EmployeeRepository dao;
@BeforeEach
public void init() {
MockitoAnnotations.openMocks(this);
}
@Test
void testFindAllEmployees() {
List<Employee> list = new ArrayList<Employee>();
Employee empOne = new Employee("John", "John");
Employee empTwo = new Employee("Alex", "kolenchiski");
Employee empThree = new Employee("Steve", "Waugh");
list.add(empOne);
list.add(empTwo);
list.add(empThree);
when(dao.findAll()).thenReturn(list);
//test
List<Employee> empList = service.findAll();
assertEquals(3, empList.size());
verify(dao, times(1)).findAll();
}
@Test
void testCreateOrSaveEmployee() {
Employee employee = new Employee("Lokesh", "Gupta");
service.save(employee);
verify(dao, times(1)).save(employee);
}
}
7.3 dao层
需要准备数据到数据库,测试复杂sql编写是否正确。
这个层的测试分支覆盖情况工具会统计的不全面,特别是使用mysql的时候,需要开发自觉测试各种情况。
注解@AutoConfigureTestDatabases可以配置使用内存数据库
@DataJpaTest 会禁用auto-configuration,只配置JPA测试需要用到的配置。
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class DaoTests {
@Autowired
EmployeeRepository employeeRepository;
@Test
public void testCreateReadDelete() {
Employee employee = new Employee("Lokesh", "Gupta");
employeeRepository.save(employee);
Iterable<Employee> employees = employeeRepository.findAll();
Assertions.assertThat(employees).extracting(Employee::getFirstName).containsOnly("Lokesh");
employeeRepository.deleteAll();
Assertions.assertThat(employeeRepository.findAll()).isEmpty();
}
}
7.4 其它工具类
使用utils类
大部分都是单元测试,尽量不要添加“@SpringBootTest”注解
7.5 集成测试
测试整个路径,包括从controller接收请求并返回,到数据库修改。
使用@SpringBootTest