最近写了一个日期工具方法
getCalenda()
。
当传入一个时间范围(两个日期值)和默认值时,他会用 Map 返回中间每一天的日期,和默认值。
给这个工具类基于 JUnit 5 写了单元测试
注意:为了规范使用,我使用 LocalDate 作为传入日期的类型。
例如:
//定义两个日期
LocalDate a=LocalDate.of(2021,8,7),
b=LocalDate.of(2021,9,7);
//传入日期和默认值 0
Map<LocalDate,? extends Number> map=DateHandlerUtil.getCalendar(a,b,Integer.parseInt("0"));
返回:
key:2021-08-07 value:0
key:2021-08-08 value:0
key:2021-08-09 value:0
...........
key:2021-09-07 value:0
那么如果我要给这个日期工具方法写单元测试该怎么写呢?
一、单元测试设计
这里我们按照契约式设计来设计我们的单元测试,主要包括 前置条件、后置条件、不变式 三个部分。
1.前置条件
调用时需要遵守的规则,就是不应该输入什么值。
这里的前置条件就是输入的两个日期起始日期不能大于结束日期。其次就是两个日期均不能为 null 值。
2.提示
提示就是不满足前置条件给什么提示(报错)
这里的提示我们这样来定义—:
- 如果起始日期大于结束日期,我们就抛出
throw new IllegalArgumentException("getCalendar 输入日期不合法,截止日期小于起始日期");
的异常。 - 如果输入的日期有 null 值,我们就抛出
throw new IllegalArgumentException("getCalendar 输入日期不合法,有空值");
异常。
3.后置条件
方法能实现什么功能,输出什么。
这里我们会返回从起始日期到结束日期的每一天的日期和默认值,组合成一个 Map 进行返回
4.不变式
进行中必须要遵守的条件,比如:x 值不能为负值
在这个函数中的不变式为
- 所有的日期必须小于等于结束日期且大于等于起始日期
- 所有的默认值必须等于输入的默认值
至此我们明确了所有需要进行测试的问题,下面我们就开始撰写单元测试
二、撰写单元测试
0.导入JUnit 5
JUnit 5 的 @Test 注解被移到了一个新的包中(org.junit.jupiter.api),在 maven 中添加 JUnit 5 的依赖包。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
1.测试后置条件
这个函数执行后结果一定是什么,什么结果是这个函数执行之后不可能出现的?
以我的日期函数为例,这个函数肯定会返回含有一个日期的 map,且 map 中 value 的类型和输入的类型一定是一致的。
@Test
public void getCalendarTest0() throws ParseException {
LocalDate a=LocalDate.of(2021,6,1),
b=LocalDate.of(2021,10,1);
Map<LocalDate,? extends Number> map=DateHandlerUtil.getCalendar(a,b,Integer.parseInt("1"));
//返回的日期至少会有一天,所以 map 不应该为空
assertNotNull(map,"日期 map 是空的");
//创建日期的数量应该等于两个日期之间的距离+1
long distance= ChronoUnit.DAYS.between(a,b);
assertTrue(map.size()==distance+1,"未能创建完整的日期列表");
for(Map.Entry entry:map.entrySet()){
assertEquals(a,entry.getKey(),"日期错误");
assertEquals(0,entry.getValue(),"默认值错误");
a=a.plusDays(1);
}
}
这里用到了assertNotNull()
和assertEquals()
两个方法,前置用于断言是否为空,后置用于断言参数是否等于某个值
2.测试前置条件
使用这个函数,哪些入参是应该报错的?
对于日期函数来说,入参是开始时间、结束时间、默认值。因此前置条件为——开始时间不应该大于结束时间,两个时间也不能有 null 值。
@Test
public void getCalendarTest1() {
LocalDate a=LocalDate.of(2021,11,1),
b=LocalDate.of(2021,10,1),
c=null;
//测试截止日期小于起止日期的情况
Throwable throwable1=assertThrows(IllegalArgumentException.class, ()->DateHandlerUtil.getCalendar(a,b,0),"起始日期大于结束日期异常未被发现");
assertEquals("getCalendar 输入日期不合法,截止日期小于起始日期",throwable1.getMessage(),"异常返回不一致");
//Local为null的情况
Throwable throwable2=assertThrows(IllegalArgumentException.class,()->DateHandlerUtil.getCalendar(a,c,0),"输入日期不合法异常未被捕获");
assertEquals("getCalendar 输入日期不合法,有空值",throwable2.getMessage());
}
这里用到了assertThrows()
,他可以判断抛出的异常和我们定义的异常类型是否一致。和assertEquals()
一起使用还可以对抛出异常的内容进行比较。
3.测试不变式
这个函数会始终确保什么条件为真?
@Test
public void getCalendarTest2() throws ParseException {
Double num1=Double.valueOf(10.0);
Integer num2=Integer.valueOf(123);
Short num3=Short.valueOf("1");
final LocalDate a=LocalDate.of(2021,9,1),
b=LocalDate.of(2021,10,1);
//测试不同的输入值
Map<LocalDate,Double> map=DateHandlerUtil.getCalendar(a,b,num1);
Map<LocalDate,Integer> map2=DateHandlerUtil.getCalendar(a,b,num2);
//不变式测试
map.forEach((key,value)->
{
assertTrue(key.equals(a)||key.isAfter(a),"生成日期比起始日期小");
assertTrue(key.equals(b)||key.isBefore(b),"生成日期比截止日期大");
assertTrue(value.equals(num1),"默认值初始化错误");
});
map2.forEach((key,value)->
{
assertTrue(key.equals(a)||key.isAfter(a),"生成日期比起始日期小");
assertTrue(key.equals(b)||key.isBefore(b),"生成日期比截止日期大");
assertTrue(value.equals(num2),"默认值初始化错误");
});
}
三、开始单元测试
右键运行我们的测试类,如果你需要查看单元测试的代码覆盖率,也可以像我这样点击with Coverage
我们还可以在@Test下面添加@DisplayName("")
注解来给每个测试方法命名。这个名字最后会输出在结果里。这里我给一个方法命名成了——“测试”
四、总结
JUnit 5 + 契约式设计 可以很容易测试需要测试的内容,帮助我们提升代码质量。
评论区