日常測試工作中,除了正常值的測試,我們還需要對參數的異常值進行測試,這其中要問起來,很多人都可以脫口而出需要測試
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;
}
測試結果:
- 結果分析:
可以看到,倘若開發沒有對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");
}
}
測試結果:
- 結果分析:
由測試結果可以看到對於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
,也是我們的測試點之一
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); }
在對ConcurrentHashMap
進行null測試時,出現了空指針異常,查看源碼發現如下:
結果分析:
從源碼中可以看到,ConcurrentHashMap
的key
,value
均不可爲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();
}
測試結果:
結果分析:
可見list
入參如果沒有做null
的判斷處理,也有空指針異常的風險,因此測試中也是我們需要關注的點。
3.1.5 總結
- 參數值是
Integer
等包裝類型,使用時因爲自動拆箱出現了空指針異常; - 字符串比較出現空指針異常;
- 諸如
ConcurrentHashMap
這樣的容器不支持Key
和Value
爲null
,強行 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
三種狀態。
爲解決上述問題,可能需要開發將DTO
和 Entity
進行拆分:
- 提前和客戶端進行一些業務邏輯確認,在
DTO
中對客戶端傳來的數據區分是不傳數據還是故意傳null
; - 在
Entity
中對字段進行註解,設置數據庫保存數據爲NOT NULL
或像時間這種設置爲由數據庫生成創建時間。
4、抽象測試場景
說了一大堆,其實抽象爲測試場景時卻很簡單,但是希望可以透過現象看本質,很可能同樣的觸發場景,其背後的觸發原因不盡相同,做好測試,沒有想象的那麼簡單~
抽象出測試場景:
上述中說明的測試過程中需要對傳參本身不傳和傳null的邏輯進行區分測試,得到不同的預期結果:
- 不傳-可能是不想更新此字段
- 傳null-可能希望把此字段置爲空,但是需經過判斷處理,最終保存爲空字符串
- 特殊字段-類似年齡這種,本身是有邏輯限制的,不可能爲空;出生年月也不可能隨意重置,必須限制輸入符合校驗規則的有效值
5、參考
本文的主要知識點參考了《極客時間》-朱曄的專欄《Java業務開發常見錯誤100例》中的
《空值處理:分不清楚的null和惱人的空指針》一文
文章詳細介紹的問題原因和建議開發解決方案,有想了解的可自行搜索,定有收穫~