系统---模块---子模块,子模块中不可分割的程序单元的测试,单元的粒度根据实际情况可能是 类或方法等。
面向对象编程中,最小单元就是方法。
单元测试目的是在集成测试和功能测试之前对系统可测试单元进行逐一检查和验证。
单元测试基本原则
Automatic自动化
单元测试应该是全自动执行,测试用例通常会被频繁地触发执行。
单元测试不允许使用System.out人工验证,而必须使用断言来验证
Independent独立性
用例之间不允许相互调用,也不允许执行次序的先后依赖
如下testMethod2需要调用testMethod2会导致运行效率降低
java
@Test
public void testMethod1(){
}
@Test
public void testMethod2(){
testMethod1();
}
主流测试框架中,JUnit的用例执行顺序是无序的
TestNG支持测试用例的顺序执行(默认测试类用例按照字典升序执行,也可以通过XML或注解Priority方式来配置执行顺序)
Repeatable可重复性
不受外界影响,比如单元测试通常被放到持续集成中,每次有代码提交,单元测试都会被触发。
为了保证被测试模块的交付质量,需要符合BCDE原则
- Border边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序
- Correct正确输入并得到预期结果
- Design与设计文档相结合
- Error证明程序有错,如强制输入非法数据、异常流程,非业务允许输入来得到预期错误结果
Mock
弥补一些不具备的因素
- 功能因素,比如被测试方法内部调用的功能不可用
- 时间因素,比如双十一还没有到来,与此时间相关的功能点
- 环境因素,支付宝政策、多端环境PC或手机
- 数据因素,线下数据样本太小,难以覆盖真实场景
- 其他因素,负载的依赖采用Mock方式实现
最简单的Mock方式是硬编码
最优雅的是使用配置文件
最佳方式是使用Mock框架,例如JMockit/EasyMock/JMock等
单元测试覆盖率
粗粒度
类覆盖:类中只要有方法或变量被测试用例调用或执行到
方法覆盖:测试用例执行过程中,某个方法被调用了。
细粒度
①、行覆盖:也称语句覆盖,度量可执行语句是否被执行到。
公式:执行到语句的行数/总的可执行语句行数
java
public class ConverageSampleMethods{
public Boolean testMethod(int a,int b,int c){
boolean result = false;
if(a==1 && b==2 || c==3){
result = true;
}
return result;
}
}
以上方法有5个可执行语句和3个入参
java
@Test
@DisplayName("line converage sample test")
void testLineConverageSample(){
CoverageSampleMethods coverageSampleMethods = newCoverageSampleMethods();
Assertions.assertTrue(coverageSampleMethods.testMethod( 1 , 2, 0));
}
以上测试用例的行覆盖率是 100% ,但是在执行过程中 c==3 的条件判断根本没有被执行到, a!=I 并且 c!=3 的情况难道不应该测试一下吗?由此可见,行覆盖的覆盖强度并不高,但由于容易计算,因此在主流的覆盖率工具中,它依然是一个十分常见的参考指标。
②、分支覆盖:也称为判定覆盖,用来度量程序中每 个判定分支是否都被执行到。
公式:代码中被执行到的分支数/所有分支的总数
③、条件判定覆盖:要求设计足够的测试用例,能够让判定中每个条件的所有可能情况至少被执行一次 同时每个判定本身的所有可能结果也至少执行一次。
java
@ParameterizedTest//定义一个参数化测试
@DisplayName("Condition Decision coverage sample test result true")
@CsvSource({
"0,2,3",
"1,0,3",
})//通过定义一个Stirng数组来定义多次运行时的参数列表,
void testConditionDecisionCoverageTrue(int a,int b,int c){
CoverageSampleMethods coverageSampleMethods = newCoverageSampleMethods() ;
Assertions assertTrue(coverageSampleMethods.testMethod(a, b, c)) ;
}
@DisplayName("Condition Decisior coverage sample test result false")
void testConditionDecisionConverageFalse(){
CoverageSampleMethods coverageSampleMethods = newCoverageSampleMethods() ;
Assertions assertTrue(coverageSampleMethods.testMethod(0, 0, 0));
}
④、条件组合覆盖:是指判定中所有条件的各种组合情况都出现至少一次。
java
@ParameterizeTest
@DisplayName("Multiple Condition Converage sample test result true")
@CsvSource({
"1,2,3",
"1,2,0",
"1,0,3",
"0,2,3",
"0,0,3",
})
void testMultipleConditionConverageSampleTrue(int a,int b,int c){
CoverageSampleMethods coverageSampleMethods= new CoverageSampleMethods();
Assertions.assertTrue(coverageSampleMethods.testMethod(a,b,c)) ;
}
@ParameterizeTest
@DisplayName("Multiple Condition Converage sample test result false")
@CsvSource({
"1,0,0",
"0,0,0",
"0,2,0",
})
void testMultipleConditionConverageSampleTrue(int a,int b,int c){
CoverageSampleMethods coverageSampleMethods= new CoverageSampleMethods();
Assertions.assertTrue(coverageSampleMethods.testMethod(a,b,c)) ;
}
这组测试用例同时满足了( a= !, b2, c3 )为( true, true, true )、( true,true, false )、( true, false, true )、( true, false, false )、( false, true, true )、( false,true, false )、( false, false, true )、( false, false , false )这 种情况。对于一个包含了 个条件的判定 至少需要 个测试用例才可以。虽然这种覆盖足够严谨,但无疑给编写测试用例增加了指数级的工作量。
⑤、路径覆盖:要求能够测试到程序中所有可能的路径
在 testMethod 方法中,可能的路径有 a1,b2,c==3 b!=2 c! =3 a! ,c !=1 c! =3种。当存在"||"时 如果第一个条件已经为 true不再计算后边表达式的值。而当存在"&& 时,如果第一个条件已经为 false同样不再计算后边表达式的值。满足路径覆盖的测试用例如下
java
@ParameterizeTest
@DisplayName("Path coverage sample test result true")
@CsvSource({
"1,2,0",
"1,0,3",
"0,0,3",
})
void testMultipleConditionConverageSampleTrue(int a,int b,int c){
CoverageSampleMethods coverageSampleMethods= new CoverageSampleMethods();
Assertions.assertTrue(coverageSampleMethods.testMethod(a,b,c)) ;
}
单元测试编写
JUnit 和TestNG 几乎始终处于市场前两位单元测试框架
JUnit常用的注解
测试用例的方法命名规范:
- 传统方式test开头,testDecodeUserTokenSuccess
- should---When,shouldSuccessWhenDecodeUserToken
java
//定义一个测试类并指定用例在测试报告展示的名称
@DisplayName("售票器类型测试")
public class TicketSellerTest{
//定义待测类的实例
private TicketSeller ticketSeller;
/**
定义在整个测试类开始前执行
通常包括全局和外部资源(包括测试桩)的创建和初始化
*/
@BeforeAll
public static void init(){
}
/**
定义在整个测试类完成后执行的操作
通常包括全局和外部资源的释放和销毁
*/
@AfterAll
public static void cleanup(){
}
/**
定义在每个测试用例开始前执行的操作
通常包括基础数据和运行环境的准备
*/
@BeforeEach
public void create(){
this.ticketSeller = new TicketSeller();
}
/**
定义在每个测试用例完成后的操作
通常包括运行环境的清理
*/
@AfterEach
public void destroy(){
}
/**
测试用例,当车票售出后余额
测试用例,余额不足应该报错
*/
@Test
@DisplayName("售票后余票应减少")
public void shouldReduceInventoryWhenTicketSoldOut(){
ticketSeller.setInventory(10);
ticketSeller.sell(1);
assertThat(ticketSeller.getInventory()).isEqualTo(9);
}
@Test
@DisPlayName("余票不足应报错")
public void shoudThrowExceptionWhenNoEnoughInventory(){
ticketSeller.setInventory(0);
assertThatExceptionOfType(TicketException.class)
.isThrownBy(()->{dicketSeller.sell(1)})
.withMessageContaining("all ticket sold out")
.withNoCause();
}
/**
Disabled将金庸测试用例
该测试用例会出现在最终的报告中,但不会被执行
*/
@Disabled
@Test
@DisplayName("有退票时余额应增加")
public void shouldIncreateInventoryWhenTicketRefund(){
ticketSeller.setInventory(10);
ticketSeller.refund(1);
assertThat(ticketSeller.getInventory()).isEqualTo(11);
}
}
对于使用Maven或Gradle等命令方式运行单元测试,该注解的内容被忽略,单元测试出错
当测试用例比较多,为了更好组织测试的结构
推荐使用JUnit的@Nested注解表达层次关系
java
@DisplayName("交易服务测试")
public class TransactioServiceTest{
@Nested
@DisplayName("用户交易测试")
class UserTransactionTest{
@Nested
@DisplayName("正向测试用例")
class PositiveCase(){
}
@Nested
@DisplayName("负向测试用例")
class NegativeCase(){
}
}
@Nested
@DisplayName("商家交易测试")
class CompanyTransactionTest{
}
}
使用分组测试,在不同场景下选择执行响应的测试用例
- 执行很快且很重要的冒烟测试用例
- 执行很慢但同样比较重要的日常测试用例
- 数量很多但不太重要的回归测试用例
java
@DisplayName("售票器类型测试")
public class TicketSellerTest{
@Test
@Tag("fast")
@DisplayName("售票后余票应减少")
public void shouldReduceInventoryWhenTicketSoldOut(){
}
@Test
@Tag("slow")
@DisplayName("一次性购买20张车票")
public void shouldSuccessWhenBuy20TicketsOnce(){
}
}
通过标签选择执行用例类型
在Maven中可以通过配置maven-surefire-pIugin插件来实现
xml
<build>
<plugins>
<plugin>
<artifactid>maven-surefire-plugin</artifactid>
<version>2.22.0</version>
<configuration>
<properties>
<includeTags>fast</includeTags>
<excludeTags>slow</excludeTags>
</properties>
</configuration>
</plugin>
</plugins>
</build>
在Gradle中通过JUnit专用的junitPlatform配置来实现
json
junitPlatForm{
filters{
engines{
include 'junit-jupiter','junit-vintage'
}
tags{
include 'fast'
exclude 'slow'
}
}
}
使用@TestFactory注解将数据的输入和输出与测试逻辑分开
java
@DisplayName("售票器类型测试")
public class ExchangeRateConverterTest{
@TestFactory
@DisplayName("时间售票检查")
Stream<DynamicTest> oddNumberDynamicTestWithStream(){
ticketSeller.setCloseTime(LocalTime.of(12,20,25,0));
return Stream.of(
Lists.list("提前购票",LocalTime.of(12,20,24,0),true),
Lists.list("准点购买",LocalTime.of(12,20,25,0),true),
Lists.list("晚点购票",LocalTime.of(12,20,26,0),false);
).map(
data -> DynamicTest.dynamicTest((String)data.get(0),()->assertThat((ticketSeller.cloudSellAt(data.get(1)))
.ieEqualTo(data.get(2))
);
}
}
断言与假设
常用的断言方法
静态方法
除了JUnit断言,还有AssertJ流式断言
java
/**
使用JUnit断言
*/
public class JUnitSampleTest{
@Test
public void testUsingJunitAssertThat(){
//字符串判断
String s = "abcde";
Assertions.assertTrue(s.startsWith("ab"));
Assertions.assertTrue(s.endsWith("de"));
Assertions.assertEquals(5,s.length());
//数字判断
Integer i = 50;
Assertions.assertTrue(i > 0);
Assertions.assertTrue(i < 100);
//日期判断
Date date1 = new Date();
Date date2 = new Date(date1.getTime() + 100);
Date date3 = new Date(date1.getTime() - 100);
Assertions.assertTrue(date1.before(date2));
Assertions.assertTrue(date1.after(date3));
//List判断
List<String> list = Arrays.asList("a","b","c","d");
Assertions.assertEquals("a",list.get(0));
Assertions.assertEquals(4,list.size());
Assertions.assertEquals("d",list.get(list.size() - 1))
//Map判断
Map<String,Object> map = new HashMap<>();
map.put("A",1);
map.put("B",1);
map.put("C",1);
Set<String> set = map.keySet();
Assertions.assertEquals(3,map.size());
Assertions.assertTrue(set.containsAll(Arrays.asList("A","B","C")));
}
}
java
/**
使用AssertJ的断言
*/
public class AssertJSampleTest{
@Test
public void testUsingAssertJ(){
//字符串判断
String s = "abcde";
Assertions.assertThat().as("字符串判断:判断首尾及长度")
.startsWith("ab").endsWith("de").hasSize(5);
//数字判断
Integer i = 50;
Assertions.assertThat(i).as("数字判断")
.isGreaterThan(0).isLessThan(100);
//日期判断
Date date1 = new Date();
Date date2 = new Date(date1.getTime() + 100);
Date date3 = new Date(date1.getTime() - 100);
Assertions.assertThat(date1).as("日期判断:日期大小比较")
.isBefore(date2).isAfter(date3);
}
//List判断
List<String> list = Arrays.asList("a","b","c","d");
Assertions.assertThat(list)
.as("List的判断:首尾元素及长度").startsWith("a")
.endsWith("d").hasSize(4);
//Map判断
Map<String,Object> map = new HashMap<>();
map.put("A",1);
map.put("B",1);
map.put("C",1);
Assertions.assertThat(map).as("Map的判断:长度及key值")
.hasSize(3).containsKeys("A","B","C");
}