昨天我們學會了如何透過 JUnit 5 與 Mockito 快速產出單元測試框架。然而在軟體工程中,很多人常陷入一種「高覆蓋率的假象」:行覆蓋率(Line Coverage)看似達到了 90%,但一到線上環境,使用者傳入奇怪的字元、邊界數值或特定時間戳時,系統依然瞬間崩潰。
原因在於:開發者自己寫測試時,往往存在思維慣性,只會測試「自己預想得到的情境」。
在軟體測試理論中,最容易爆出 Bug 的地方往往集中在**「邊界(Boundaries)」**——例如:0 與 1 之間、集合為空或剛好滿載、整數最大值(Integer.MAX_VALUE)、以及日期閏年切換。
今天我們就來示範:如何讓 ChatGPT 扮演挑剔的「QA 邊界破壞者」,丟入一段既有代碼,讓 AI 主動揪出隱藏的邊界盲點,並透過 JUnit 5 的**參數化測試(@ParameterizedTest)**一口氣補全測試防線!
想要讓 AI 幫你找出思考盲區,可以使用以下「逆向測試」提問架構:
【邊界案例挖掘提示詞範本】
請扮演資深 QA 自動化測試架構師與軟體測試黑客。以下是一段已經撰寫完成的業務邏輯代碼:
[貼上既有的業務方法]
請依序進行邊界分析與補全:
@ParameterizedTest 與 @CsvSource / @MethodSource 撰寫高密度的邊界測試案例,一次覆蓋所有極端情境。假設我們有一個根據消費金額與是否為生日月份計算折扣的業務方法:
[待測商業邏輯代碼]
public class LoyaltyDiscountCalculator {
/**
* 計算折抵後的最終金額
* @param totalAmount 原始金額
* @param isBirthday 是否為生日當月
* @param memberPoints 現有紅利點數 (每 10 點可折抵 1 元,最高折抵金額的 20%)
*/
public double calculateFinalPrice(double totalAmount, boolean isBirthday, int memberPoints) {
if (totalAmount <= 0) {
return 0.0;
}
// 滿千送百
double discount = Math.floor(totalAmount / 1000) * 100;
// 生日當月額外享 9 折
double discountedPrice = totalAmount - discount;
if (isBirthday) {
discountedPrice = discountedPrice * 0.9;
}
// 點數折抵:最高折抵當前金額 20%
double maxPointDeduction = discountedPrice * 0.2;
double actualPointDeduction = Math.min(memberPoints / 10.0, maxPointDeduction);
return discountedPrice - actualPointDeduction;
}
}
當我們請 AI 進行邊界審查時,它列出了開發者極易忽略的三大漏洞:
double 進行乘法與減法運算時(如 * 0.9),容易產生 199.99999999999997 的精度誤差,在金融與計價場景應全面換成 BigDecimal 或做精確四捨五入。memberPoints 為負數(例如 -50),Math.min(-5.0, maxPointDeduction) 會得出負值,反而讓消費者的折抵金額「變成加價」!999.99、1000.00、1000.01 時的折抵表現是否符合業務規範?AI 推薦使用 @ParameterizedTest 搭配 @CsvSource,用一張清晰的測試矩陣一次性覆蓋正常與極端邊界:
[JUnit 5 參數化測試實作]
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
class LoyaltyDiscountCalculatorTest {
private final LoyaltyDiscountCalculator calculator = new LoyaltyDiscountCalculator();
@ParameterizedTest(name = "[{index}] 金額={0}, 生日={1}, 點數={2} => 預期最終價={3}")
@CsvSource({
// --- 1. 金額邊界測試 ---
"0.0, false, 0, 0.0", // 金額為 0
"-100.0, false, 0, 0.0", // 負數金額防呆
"999.0, false, 0, 999.0", // 剛好未達滿千門檻
"1000.0, false, 0, 900.0", // 剛好達到滿千折百 (1000 - 100)
"1000.1, false, 0, 900.1", // 剛好超過滿千
// --- 2. 生日折扣邊界 ---
"1000.0, true, 0, 810.0", // 滿千折百後再打 9 折: (1000 - 100) * 0.9 = 810.0
// --- 3. 點數折抵上限邊界 (最高折抵 20%) ---
"1000.0, false, 500, 850.0", // 900 元,上限 20% 為 180 元。傳入 500 點可折 50 元 (未達上限)
"1000.0, false, 2000, 720.0", // 傳入 2000 點可折 200 元,但觸碰上限 180 元,應只折 180 (900 - 180 = 720)
"1000.0, false, 0, 900.0" // 0 點數情況
})
@DisplayName("驗證各類金額、生日優惠與點數折抵之邊界案例")
void calculateFinalPrice_BoundaryCases(double totalAmount, boolean isBirthday, int memberPoints, double expectedPrice) {
// [Act]
double actualPrice = calculator.calculateFinalPrice(totalAmount, isBirthday, memberPoints);
// [Assert] 使用 within 容許微小的雙精度浮點誤差
assertThat(actualPrice).isCloseTo(expectedPrice, within(0.001));
}
}
@Test 方法,代碼冗長且難維護;透過 @ParameterizedTest,只需維護一張簡潔的 CSV 測試矩陣,幾行代碼就能涵蓋十幾種邊界情況。今天我們掌握了利用 AI 挖掘程式碼邊界盲區的方法,並透過 JUnit 5 參數化測試架構建立了高密度的防禦測試網。