【测试原理与设计】理解常见异常值测试-null、空值

日常测试工作中,除了正常值的测试,我们还需要对参数的异常值进行测试,这其中要问起来,很多人都可以脱口而出需要测试null空值等等。但是要问为什么要测?具体怎么测?测试的结果说明了什么问题?可能就不尽然能说的清楚了,我自己也是遇到过这种情况,反问自己这三个问题,说的出来一些,又好像不是那么确定,心里还是有点虚~

1、Bug现象

日常测试或生活中可能会出现如下现象:

  • 登录用户,用户名处显示“你好,尊敬的用户null”。
  • 查看商品信息,显示商品信息为,商品名:null,价格:null。
  • 发送短信,短信信息显示“用户null,你的验证码为xxx”。

这些现象对于普通用户来说,可能会产生疑惑,对于我们IT工程师来说,瞬间就会激起兴趣,
马上就能get到点。程序获取信息失败,又没有处理好null,把空格式化成了null。像这种还仅仅是显示的问题,有些null值可能会造成应用崩溃或逻辑错误,这对于测试来说可是个大单,这是bug,咱么测试要提!

2、关于null

空指针异常虽然恼人但好在容易定位,更麻烦的是要弄清楚 null 的含义。比如,客户端给服务端的一个数据是 null,那么其意图到底是给一个空值,还是没提供值呢?再比如,数据库中字段的 NULL 值,是否有特殊的含义呢,针对数据库中的 NULL 值,写 SQL 需要特别注意什么呢?

3、透过现象看本质

3.1 null造成的空指针异常

3.1.1 Integer参数的自动拆箱

我们学习Java都知道Java中对于基本数据类型和包装类是可以通过自动装箱拆箱来进行相互转换的,那么看一下下面这个简单的例子

Integer类型的变量a赋值null,然后再传入getInt方法,+1后赋值给int类型的变量b

@Test
void testIntegerNull(){
    Integer a = null;
    getInt(a);
}

private void getInt(Integer a){
    int b = a + 1;
}

测试结果:
【测试原理与设计】理解常见异常值测试-null、空值_第1张图片

  • 结果分析:
    可以看到,倘若开发没有对Integer类型的参数做null处理的话,就有可能造成空指针异常,因此这里我认为需要对Integer入参做null的测试验证

3.1.2 String的比较

使用equals进行字符串的比较是最常见的业务之一了,看如下测试代码

1、对于传入getString方法的字符串a判断是否为pass,将a赋值为null传入

@Test
void testStringNull(){
    String a = null;
    getString(a);
}

private void getString(String a){
    if (a.equals("pass")){
        System.out.println("PASS");
    }
}

测试结果:
【测试原理与设计】理解常见异常值测试-null、空值_第2张图片

  • 结果分析:
    由测试结果可以看到对于 String字面量的比较,倘若开发没有把字面量放在前面,就会有空指针异常的风险;如若字面量放前面,比如"pass".equals(a),这样即使 anull 也不会出现空指针异常.

2、对传入的字符串进行等值比较:

@Test
void testStringEqualsNull(){
    String a = "a";
    String A = null;
    getStringEquals(a,A);
}

private void getStringEquals(String a, String A) {
    if (A.equals(a)){
        System.out.println("pass");
    }
}

测试结果:
【测试原理与设计】理解常见异常值测试-null、空值_第3张图片

  • 结果分析:
    对于两个可能为 null 的字符串变量的 equals 比较,倘若开发未对字符串做判空处理,也会出现空指针异常

因此对于String类型的入参null,也是我们的测试点之一

3.1.3 ConcurrentHashMap的key、value

平常我们最常用的就是HashMap了,而在并发时,HashMap有其弊端,开发可能会采用ConcurrentHashMap

关于ConcurrentHashMap,我目前在这里也无法说清,这里提供一篇文章做参考学习:
HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!

  • 先来看HashMap,对其进行key,value赋值null:
    @Test
    void test(){
        HashMap<String,Object> map = new HashMap<>();
        map.put(null,null);
    }
    
    测试没有任何问题
  • 再来看ConcurrentHashMap,对其进行key,value赋值null:
    @Test
    void concurrentHashMapNull(){
        ConcurrentHashMap<String,Object> map = new ConcurrentHashMap<>();
        map.put("a",null);
        map.put(null,null);
    }
    
    测试结果:
    【测试原理与设计】理解常见异常值测试-null、空值_第4张图片
    在对ConcurrentHashMap进行null测试时,出现了空指针异常,查看源码发现如下:
    【测试原理与设计】理解常见异常值测试-null、空值_第5张图片
    结果分析:
    从源码中可以看到,ConcurrentHashMapkeyvalue均不可为null,否则就抛出空指针异常。

3.1.4 List返回为null

List也是我们最常用的集合之一了,当list为空时,查看如下测试代码

getList方法获取list并计算大小

@Test
 void testListNull(){
     List<String> list = null;
     getList(list);
 }
 private void getList(List<String> list){
     list.size();
 }

测试结果:
【测试原理与设计】理解常见异常值测试-null、空值_第6张图片
结果分析:
可见list入参如果没有做null的判断处理,也有空指针异常的风险,因此测试中也是我们需要关注的点。

3.1.5 总结

  • 参数值是 Integer 等包装类型,使用时因为自动拆箱出现了空指针异常;
  • 字符串比较出现空指针异常;
  • 诸如 ConcurrentHashMap 这样的容器不支持 KeyValuenull,强行 put null 的 Key 或 Value 会出现空指针异常;
  • A 对象包含了 B,在通过 A 对象的字段获得 B 之后,没有对字段判空就级联调用 B 的方法出现空指针异常;
  • 方法或远程服务返回的 List 不是空而是 null,没有进行判空就直接调用 List 的方法出现空指针异常。

3.2 null 未报空指针异常

使用判空或其他方式避免空指针异常,不一定是解决问题的最好方式,空指针没出现可能隐藏了更深的 Bug。因此,解决空指针异常,还是要真正 case by case 地定位分析案例,然后再去做判空处理,而处理时也并不只是判断非空然后进行正常业务流程这么简单,同样需要考虑为空的时候是应该出异常、设默认值还是记录日志等。

现在以如下一个 User 的 POJO为例,研究null的含义。此POJO同时扮演 DTO 和数据库 Entity 角色,包含用户 ID、姓名、昵称、年龄、注册时间等属性:

@Data
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;
    private String name;
    private String nickname;
    private Integer age;
    private Date createDate = new Date();
}

初始化数据:
在这里插入图片描述

3.2.1 DTO 中字段的 null

客户端现在想将用户id为1的用户name设置为null,于是通过更新接口传来数据,仅传id和那么字段:

{ "id":1, "name":null}

接口返回后的数据结果:
在这里插入图片描述
结果分析:

  • 对于客户端而言,需要更新的数据进行传入,不传的字段就代表需要保留原有值,传了null就意味着要重置这个值
    而例子中调用方只希望重置用户名,但age也被设置成了null
    在这里插入图片描述
    这也是因为后端未对这两种情况做区分处理,所以测试中,接口除了要校验字段的null值,还要测试字段本身就不传
  • 例子中创建时间字段create_date的时间也发生了改变,因为POJO 中的字段有默认值。如果客户端不传值,就会赋值为默认值,导致创建时间也被更新到了数据库中;
    在这里插入图片描述
    在这里插入图片描述
    这显然不符合我们的测试预期,例如订单的创建时间是固定的,不可能再发生更改,因此测试时需要关注。
  • 更新后结果中,字段nickname也发生了变化,原本的需求逻辑应该是访客类型+name组成nickname,而这里由于后端可能未做处理而造成了在格式化字符串时把null值格式化了null字符串
    在这里插入图片描述
    在这里插入图片描述

3.2.2 Entity中设置数据库NOT NULL

数据库字段允许保存 null,会进一步增加出错的可能性和复杂度。因为如果数据真正落地的时候也支持 NULL 的话,可能就有 NULL空字符串字符串 null 三种状态。

为解决上述问题,可能需要开发将DTOEntity 进行拆分:

  • 提前和客户端进行一些业务逻辑确认,在DTO中对客户端传来的数据区分是不传数据还是故意传null
  • Entity中对字段进行注解,设置数据库保存数据为NOT NULL或像时间这种设置为由数据库生成创建时间。

4、抽象测试场景

说了一大堆,其实抽象为测试场景时却很简单,但是希望可以透过现象看本质,很可能同样的触发场景,其背后的触发原因不尽相同,做好测试,没有想象的那么简单~

抽象出测试场景:
上述中说明的测试过程中需要对传参本身不传和传null的逻辑进行区分测试,得到不同的预期结果:

  • 不传-可能是不想更新此字段
  • 传null-可能希望把此字段置为空,但是需经过判断处理,最终保存为空字符串
  • 特殊字段-类似年龄这种,本身是有逻辑限制的,不可能为空;出生年月也不可能随意重置,必须限制输入符合校验规则的有效值

5、参考

本文的主要知识点参考了《极客时间》-朱晔的专栏《Java业务开发常见错误100例》中的
《空值处理:分不清楚的null和恼人的空指针》一文
文章详细介绍的问题原因和建议开发解决方案,有想了解的可自行搜索,定有收获~

你可能感兴趣的:(软件测试,测试原理和设计,软件测试,接口测试,测试原理,null值处理)