日常测试工作中,除了正常值的测试,我们还需要对参数的异常值进行测试,这其中要问起来,很多人都可以脱口而出需要测试
null
、空值
等等。但是要问为什么要测?具体怎么测?测试的结果说明了什么问题?可能就不尽然能说的清楚了,我自己也是遇到过这种情况,反问自己这三个问题,说的出来一些,又好像不是那么确定,心里还是有点虚~
日常测试或生活中可能会出现如下现象:
这些现象对于普通用户来说,可能会产生疑惑,对于我们IT工程师来说,瞬间就会激起兴趣,
马上就能get到点。程序获取信息失败,又没有处理好null,把空格式化成了null。像这种还仅仅是显示的问题,有些null值可能会造成应用崩溃或逻辑错误,这对于测试来说可是个大单,这是bug,咱么测试要提!
空指针异常虽然恼人但好在容易定位,更麻烦的是要弄清楚 null
的含义。比如,客户端给服务端的一个数据是 null
,那么其意图到底是给一个空值
,还是没提供值呢?再比如,数据库中字段的 NULL
值,是否有特殊的含义呢,针对数据库中的 NULL
值,写 SQL
需要特别注意什么呢?
我们学习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;
}
Integer
类型的参数做null
处理的话,就有可能造成空指针异常,因此这里我认为需要对Integer
入参做null
的测试验证使用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");
}
}
String
和字面量
的比较,倘若开发没有把字面量放在前面,就会有空指针异常的风险;如若字面量放前面,比如"pass".equals(a)
,这样即使 a
是 null
也不会出现空指针异常.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
的字符串变量的 equals
比较,倘若开发未对字符串做判空处理,也会出现空指针异常因此对于String
类型的入参null
,也是我们的测试点之一
平常我们最常用的就是
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);
}
测试结果:ConcurrentHashMap
进行null测试时,出现了空指针异常,查看源码发现如下:ConcurrentHashMap
的key
,value
均不可为null
,否则就抛出空指针异常。List也是我们最常用的集合之一了,当list为空时,查看如下测试代码
getList
方法获取list
并计算大小
@Test
void testListNull(){
List<String> list = null;
getList(list);
}
private void getList(List<String> list){
list.size();
}
测试结果:
结果分析:
可见list
入参如果没有做null
的判断处理,也有空指针异常的风险,因此测试中也是我们需要关注的点。
Integer
等包装类型,使用时因为自动拆箱出现了空指针异常;ConcurrentHashMap
这样的容器不支持 Key
和 Value
为 null
,强行 put null 的 Key 或 Value 会出现空指针异常;List
不是空而是 null
,没有进行判空就直接调用 List
的方法出现空指针异常。使用判空或其他方式避免空指针异常,不一定是解决问题的最好方式,空指针没出现可能隐藏了更深的 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();
}
客户端现在想将用户id为1的用户name设置为null,于是通过更新接口传来数据,仅传id和那么字段:
{ "id":1, "name":null}
null
就意味着要重置这个值age
也被设置成了null
;null
值,还要测试字段本身就不传create_date
的时间也发生了改变,因为POJO 中的字段有默认值。如果客户端不传值,就会赋值为默认值,导致创建时间也被更新到了数据库中;nickname
也发生了变化,原本的需求逻辑应该是访客类型
+name
组成nickname
,而这里由于后端可能未做处理而造成了在格式化字符串时把null值
格式化了null字符串
数据库字段允许保存
null
,会进一步增加出错的可能性和复杂度。因为如果数据真正落地的时候也支持NULL
的话,可能就有NULL
、空字符串
和字符串 null
三种状态。
为解决上述问题,可能需要开发将DTO
和 Entity
进行拆分:
DTO
中对客户端传来的数据区分是不传数据还是故意传null
;Entity
中对字段进行注解,设置数据库保存数据为NOT NULL
或像时间这种设置为由数据库生成创建时间。说了一大堆,其实抽象为测试场景时却很简单,但是希望可以透过现象看本质,很可能同样的触发场景,其背后的触发原因不尽相同,做好测试,没有想象的那么简单~
抽象出测试场景:
上述中说明的测试过程中需要对传参本身不传和传null的逻辑进行区分测试,得到不同的预期结果:
本文的主要知识点参考了《极客时间》-朱晔的专栏《Java业务开发常见错误100例》中的
《空值处理:分不清楚的null和恼人的空指针》一文
文章详细介绍的问题原因和建议开发解决方案,有想了解的可自行搜索,定有收获~