JWT要怎麼測試?要寫那些測試?JWT 很容易寫出一堆測試:簽發、解析、過期、簽章錯誤、登出黑名單等等,但其中不少只是在重測 JWT 函式庫。
第一步不是列出所有要測試的情況,而是先定義:這件事是誰的責任?
| 測試對象 | 要確認什麼 | 測試方式 |
|---|---|---|
JwtUtils |
是否照本專案的規則簽發、解析 | 整合測試,真實金鑰 |
TokenAuthenticator |
拿到各種驗證結果後怎麼處理 | 單元測試 + mock |
JwtUtils 測的正是編碼與簽章本身,mock 掉就沒有意義。TokenAuthenticator 則相反,它關心的是「過期了怎麼辦」、「被撤銷了怎麼辦」這種情況發生後怎麼處理。
JwtUtils測試:那就真的簽一次JwtUtilsTest 是整合測試:啟動整個 Spring 應用,讀取真實的金鑰檔,簽出來的 token 跟正式環境的一模一樣。
@Test
@DisplayName("每次產出的 jti 都不同")
void jtiIsUnique() {
String token1 = jwtUtils.generateAccessToken("alice");
String token2 = jwtUtils.generateAccessToken("alice");
String jti1 = jwtUtils.parseToken(token1).getId();
String jti2 = jwtUtils.parseToken(token2).getId();
assertThat(jti1).isNotEqualTo(jti2);
}
這個測試測的不是「UUID 會不會重複」,而是簽發流程有沒有真的每次產生新的 jti。
登出時,本專案把 access token 的 jti 放進黑名單。這時如果同一個使用者的兩支 token 拿到相同的 jti,登出一台裝置,另一台也會跟著被踢出去。
TokenAuthenticator:測試 token 過期後的處理總不會為了測試token過期的情境,用 Thread.sleep(15 * 60 * 1000) 來測,真的讓測試等15分鐘吧? 常見做法有三種:
| 做法 | 適合情境 |
|---|---|
注入 Clock |
程式本身需要可控的時間 |
| 直接簽一顆已過期的 token | 想驗證函式庫判斷過期的實際行為 |
| mock 解析結果 | 只想驗證拿到過期結果後怎麼處理 |
這次採用mock 解析結果的作法:
@Test
@DisplayName("過期 token → empty")
void expiredToken_empty() {
when(jwtUtils.parseToken("expired"))
.thenThrow(new ExpiredJwtException(null, null, "expired"));
assertThat(authenticator.tryAuthenticate("expired")).isEmpty();
}
JWT 驗證順序是:解析 JWT(同時驗簽章與效期)→ 取出 jti → 查 Redis 黑名單。已經登出的 token 在黑名單上,拿它呼叫 API 會被擋下。
但 Redis 掛掉時,黑名單查不到,系統事先又不知道誰登出過,只能對所有請求二選一:
| 選擇 | 沒登出的人 | 已登出的人 |
|---|---|---|
| fail-open:查不到就放行 | 照常使用 | 舊 token 還能用 |
| fail-close:查不到就擋下 | 全部被踢出去 | 擋下 |
本專案選擇 fail-open。代價有上限:簽章與效期在查 Redis 之前就驗完了,偽造或過期的 token 照樣擋下;漏掉的只有「已經登出、但還沒過期」的 token,最多再用 15 分鐘。
@Test
@DisplayName("黑名單檢查 Redis 炸掉 → fail-open,仍回 Authentication")
void redisDown_failOpen() {
when(jwtUtils.parseToken("good")).thenReturn(claims("alice", "jti-3"));
when(tokenRedisService.isAccessTokenBlacklisted("jti-3"))
.thenThrow(new RedisConnectionFailureException("redis down"));
Optional<Authentication> result = authenticator.tryAuthenticate("good");
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("alice");
}
降級路徑平常不會被執行,因此如果沒有這條測試,某次重構把例外改成「驗證失敗」也不會有人察覺,要等 Redis 真的故障那天才發現。