侧边栏壁纸
  • 累计撰写 58 篇文章
  • 累计创建 67 个标签
  • 累计收到 1 条评论

Java 如何写单元测试

lihaocheng
2021-08-22 / 0 评论 / 0 点赞 / 958 阅读 / 3,952 字
温馨提示:
晚上记得开启夜间模式哦

最近写了一个日期工具方法 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

IMG_3923.jpg

那么如果我要给这个日期工具方法写单元测试该怎么写呢?

一、单元测试设计

这里我们按照契约式设计来设计我们的单元测试,主要包括 前置条件、后置条件、不变式 三个部分。

1.前置条件

调用时需要遵守的规则,就是不应该输入什么值。
这里的前置条件就是输入的两个日期起始日期不能大于结束日期。其次就是两个日期均不能为 null 值。

2.提示

提示就是不满足前置条件给什么提示(报错)
这里的提示我们这样来定义—:

  1. 如果起始日期大于结束日期,我们就抛出throw new IllegalArgumentException("getCalendar 输入日期不合法,截止日期小于起始日期");的异常。
  2. 如果输入的日期有 null 值,我们就抛出throw new IllegalArgumentException("getCalendar 输入日期不合法,有空值");异常。

3.后置条件

方法能实现什么功能,输出什么。
这里我们会返回从起始日期到结束日期的每一天的日期和默认值,组合成一个 Map 进行返回

4.不变式

进行中必须要遵守的条件,比如:x 值不能为负值
在这个函数中的不变式为

  1. 所有的日期必须小于等于结束日期且大于等于起始日期
  2. 所有的默认值必须等于输入的默认值
    至此我们明确了所有需要进行测试的问题,下面我们就开始撰写单元测试

二、撰写单元测试

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

截屏20210829 下午1.24.00.png

我们还可以在@Test下面添加@DisplayName("")注解来给每个测试方法命名。这个名字最后会输出在结果里。这里我给一个方法命名成了——“测试”

截屏20210829 下午2.46.06.png

四、总结

JUnit 5 + 契约式设计 可以很容易测试需要测试的内容,帮助我们提升代码质量。

0

评论区