在日常开发中离不开进行代码的测试,因此很有必要学会如何进行规范的单元测试
单元测试的好处:
1、提升软件质量
2、促进代码优化
3、提升研发效率
4、增加重构自信
单元测试基本原则:(AIR原则,必须用断言式来检测,符合BCDE原则)
1、Automatic(自动化)
2、Independent(独立性)
3、Repeatable(可重复)
4、Border,边界值测试,包含循环边界、特殊值、特殊值时间点、数据顺序
5、Correct,正确的输入,并得到预期的结果
6、Design,与设计文档像结合,来编写单元测试
7、Error,单元测试用于证明程序是有错的,需要强制进行错误的输入,来检测是否能达到预期的结果。
单元测试覆盖率
粗粒度覆盖率:类覆盖和方法覆盖(只要类和参数和方法都调用和执行到了,就说这个类被测试覆盖了)
细粒度覆盖率:
1、行覆盖:是否每行都被执行到了。
2、分支覆盖:是否每个分支都被覆盖到了
3、条件覆盖:判定中是否每一个条件的所有可能情况至少被执行一次。注意一些&&和||的短路执行,要覆盖到。
单元测试编写:
工具:
1、主流的为JUnit和TestNG(这里主要介绍JUnit)
2、JUnit注解:
@Test 注明这个方法是测试方法,JUnit在测试阶段能检测到使用该注解的方法,标明它们可以测试运行
@TestFactory 注明一个方法是基于数据驱动的动态测试数据源
@ParameterizedTest 注明是测试方法,可以让一个测试方法使用不同的入参运行多次
@RepeatedTest 注明自定义运行次数
@BeforeEach 指定每个测试方法运行前先执行该方法,一般用于数据准备
@AfterEach 每个测试方法运行后都执行该方法,一般用于删除数据(组成测试回环,新增的数据,测试完后删除)
@BeforeAll 每个测试类运行前,都运行一个指定方法
@AfterAll 每个测试类运行后,都运行一个指定方法
@Disabled 注明该测试方法不再运行
@Nested 为测试添加嵌套层级,以便组织用例结构
@Tag 为测试类或方法添加标签,以便有选择性的执行
常用的断言式:
1、fail() 断言测试失败
2、assertTrue() /assertFalse() 断言条件为真或假
3、assertNull() / assertNotNull() 断言条件为非空
4、assertEquals() / assertNotEquals() 断言指定两个值相等或不相等,使用equals方法比较
5、assertArrayEquals() 断言数组元素全部相等
6、assertSame() / assertNotSame() 断言两个对象是否相等
7、assertThrows() / assertDoesNotThrow() 断言是否抛出了一个特定类型的异常
8、assertTimeout() / assertTimeoutPreemptively() 断言是否执行超时,区别在于测试程序是否在同一个线程内执行
9、assertIterableEquals() 断言迭代器元素是否都相等
10、assertLinesMatch() 断言字符串列表中全部元素都正则匹配
11、assertAll() 断言多个条件同时满足
还有一种AssertJ断言方式(流式断言),这种方式可以进行像Java8流式编程一样,进行流式的断言处理,大家可以自行搜素了解一下,当需要断言的条件有多个的时候,流式断言十分简便。
测试实例代码(规范代码):
需要被测试的类:
/**
* 需要被测试的类
*/
public class TestClass {
public String getName(){
return "TestClass";
}
public int getInt(int i){
return i;
}
public int getSum(int a, int b){
return a+b;
}
public boolean getFalse(){
return false;
}
public boolean getTrue(){
return true;
}
}
测试类:
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayName("学习JUnit")
public class TestJUnit {
//定义待测试类的实例
private TestClass testClass ;
/**
* 定义在整个测试类开始执行前的操作
* 通过包括全局和外部资源的创建和初始化
*/
@BeforeAll
public static void init(){
}
/**
* 定义在整个测试类执行完之后执行的操作
* 通过包括全局和外部资源的释放和销毁(各种连接的注销,释放等)
*/
@AfterAll
public static void cleanup(){
}
/**
* 在每个测试用例开始前执行的操作
* 通过包括基础数据和运行环境的准备(给数据库添加数据测试等等)
*/
@BeforeEach
public void create(){
this.testClass = new TestClass();
}
/**
* 在每个测试用例完成后执行的操作
* 通常包括运行环境的清理
*/
@AfterEach
public void destory(){
}
/**
* 测试用例,测试类的求和功能
*/
@Test
@DisplayName("测试求和功能")
public void getSum(){
int a = 1;
int b = 2;
int c = testClass.getSum(a,b);
assertEquals(3,c);
}
/**
* 输入多组参数,重复测试
* @param a
* @param b
* @param ans
*/
@ParameterizedTest
@DisplayName("测试重复求和功能")
@CsvSource({
"1, 2, 3",
"1, 3, 3"
})
public void getSums(int a, int b, int ans){
int c = testClass.getSum(a,b);
assertEquals(ans,c);
}
/**
* 禁用该测试用例,该测试用例不会被执行
* 会出现在最终的报告中
*/
@Disabled
@Test
@DisplayName("测试类名是否正确")
public void getName(){
assertTrue("TestClass".equals(testClass.getName()));
}
@Nested
@DisplayName("表示属于TestJunit测试的子级测试(如:交易服务测试下的用户交易测试,一般建议不超过三层)")
class NestTest{
@DisplayName("嵌套测试")
public void nestTest(){
assertTrue(true);
}
}
/**
* 标注运行频率,
* 可以使用maven添加插件,先执行所有为fast的,后执行slow的测试用例
*/
@Test
@Tag("fast") //slow
@DisplayName("返回True")
public void getTrue(){
assertTrue(testClass.getTrue());
}
}