當我閱讀《射鵰英雄傳》時,左手畫方,右手畫圓讓我想到開發與測試的關係。這裡分享如何透過 ChatGPT 結合 TDD,以 TypeScript X Jest 編寫購物車功能
測試驅動開發(TDD)是一種軟體開發方法論,其主要目標是確保開發的代碼在品質和可靠性方面達到高水平。TDD 的核心思想是在實際編寫功能代碼之前,首先編寫測試案例。這些測試案例被用來驗證預期的功能是否正確運作。讓我們深入探討 TDD 的基本概念和步驟。
關於測試驅動開發
TDD,即測試驅動開發(Test-Driven Development),是一種軟體開發方法,其核心理念是在編寫實際功能代碼之前,先編寫測試案例。這個方法的目標是確保軟體在開發過程中具有高品質、可靠性和可維護性。以下是 TDD 的基本步驟,通常縮寫為”Red-Green-Refactor”:
編寫測試案例(Red):在這一階段,開發者明確定義新功能或現有功能的預期行為,然後創建測試案例來檢查這些期望。這些測試案例通常是自動化的單元測試,用於測試特定功能或模組的行為。這一步驟的目標是確保測試案例能夠檢測到功能缺失。
運行測試案例(Green):接下來,開發者運行這些測試案例,但由於還沒有編寫相應的功能代碼,所以測試通常會失敗。這個階段的目標是確定測試案例能夠正確地檢測到功能的缺失。
編寫功能代碼(Refactor):在確定測試案例失敗後,開發者開始編寫功能代碼,以滿足測試案例的要求。這個過程可能需要多次迭代,直到所有測試案例都成功通過。這一步驟的目標是實現功能的期望行為。
這個流程的關鍵在於它的反覆性:開發者不斷重複這三個步驟,每次都專注於一個小部分的功能或代碼,確保代碼品質和功能的穩定性。這種方法的好處在於它鼓勵小步快跑的開發方式,有助於減少錯誤並保持代碼的可維護性。
好處
當談到測試驅動開發(Test-Driven Development,TDD)的好處時,有三個關鍵方面值得深入探討。
首先,值得強調的是,測試驅動開發(Test-Driven Development,TDD)在提升軟體品質方面具有巨大的優勢。這種方法要求在實際編寫功能代碼之前,必須先編寫相對應的測試案例。這一步驟確保了代碼在開發過程中經過全面的測試,顯著降低了潛在錯誤和缺陷的風險。這有助於打造更可靠、穩定且高品質的軟體產品。同時,這些測試案例也充當了一種詳細的文檔,清晰地描述了每個功能的預期行為。這對於新加入的開發者來說特別有價值,因為他們可以輕鬆理解和修改代碼,確保代碼的一致性和品質得以維護。
其次,TDD 可以讓我們充滿信心地重構項目。開發者擁有完整的測試案例,可以更自信地進行代碼重構工作。他們可以優化代碼的結構和性能,同時確保代碼的行為不會受到負面影響。這種信心來自於測試案例的存在,這些案例可以迅速檢測是否引入了新的錯誤。這有助於保持代碼的高品質,避免技術債的不斷累積,確保軟體在最佳狀態下運行。通过 TDD,代碼重構不再是一個可怕的挑戰,而是一個可控和可預測的過程,這有助於確保軟體的持續優化和演進。
此外,TDD 還可以促進反思和不斷改進的文化。開發人員會不斷地檢討他們的測試案例,確保它們仍然能夠有效地捕捉代碼的行為。這種反思過程有助於發現和修復可能存在的問題,同時也鼓勵團隊持續學習和改進開發實踐。
實際案例:購物車
一般而言,測試驅動開發對於開發者要求過高,因爲要求開發者手動編寫測試案例,是一個比較痛苦的過程。然而,在現代技術的快速發展中,我們迎來了全新的工具和技術,ChatGPT。ChatGPT 是一種自然語言處理模型,具備自動生成測試案例的潛力,這將極大地簡化 TDD。
描述
以下是購物車應該實現的功能和要求:
-
新增商品: 使用者能夠將商品添加到購物車中,同時指定商品的數量。如果已經存在於購物車中的商品再次被添加,則其數量應該增加。
-
刪除商品: 使用者可以從購物車中刪除指定的商品。
-
更新商品數量: 使用者可以更新購物車中商品的數量。
-
計算購物車總數量: 系統能夠計算和提供購物車中所有商品的總數量。
-
計算購物車總價格: 系統能夠計算和提供購物車中所有商品的總價格。同時,如果使用了促銷代碼,應該能夠應用相應的折扣。
-
清空購物車: 使用者可以清空購物車中的所有商品。
-
處理異常情況: 系統應該能夠處理當使用者嘗試對不存在於購物車中的商品進行操作時的異常情況,如刪除或更新數量。
-
應用促銷代碼折扣: 使用者可以使用促銷代碼,並在計算總價格時應用相應的折扣。
這些功能和要求將確保購物車的正確運作,為使用者提供一個無縫且方便的購物體驗。
藉由 ChatGPT 的協助,我們能夠更迅速地產生測試案例,並透過 TDD 方法來引導我們開發購物車應用。
Calpa: “Now I need to implement the shopping cart, please use TDD to give me the test cases using Jest.”
ChatGPT 會理解我們的需求並生成以下測試案例的版本:
// ShoppingCart.test.ts
import { ShoppingCart, Item } from "./ShoppingCart";
import DiscountManager from "./DiscountManager"; // Import DiscountManager
describe('ShoppingCart', () => {
let cart: ShoppingCart;
let itemA: Item;
let itemB: Item;
let discountManager: DiscountManager; // Add DiscountManager
beforeEach(() => {
discountManager = new DiscountManager();
cart = new ShoppingCart(discountManager);
itemA = new Item(1, 'Product A', 10);
itemB = new Item(2, 'Product B', 5);
});
it('should add an item to the cart', () => {
cart.addItem(itemA, 2);
expect(cart.getTotalItems()).toBe(2);
});
it('should remove an item from the cart', () => {
cart.addItem(itemA, 2);
cart.removeItem(itemA.id);
expect(cart.getTotalItems()).toBe(0);
});
it('should update the quantity of an item in the cart', () => {
cart.addItem(itemA, 2);
cart.updateItemQuantity(itemA.id, 3);
expect(cart.getItemQuantity(itemA.id)).toBe(3);
});
it('should calculate the total price of items in the cart', () => {
cart.addItem(itemA, 2);
cart.addItem(itemB, 3);
// (10 * 2) + (5 * 3) = 20 + 15 = 35
expect(cart.getTotalPrice()).toBe(35);
});
it('should calculate the total price of items in the cart', () => {
cart.addItem(itemA, 2);
// (10 * 2) * 2
expect(cart.getTotalPrice()).toBe(40);
});
it('should clear the cart', () => {
cart.addItem(itemA, 2);
cart.clearCart();
expect(cart.getTotalItems()).toBe(0);
});
it('should handle getting quantity for an item not in the cart', () => {
expect(cart.getItemQuantity(itemA.id)).toBe(0);
});
it('should handle removing an item not in the cart', () => {
// Removing an item that's not in the cart should not throw an error
expect(() => cart.removeItem(itemB.id)).not.toThrow();
});
it('should handle updating quantity for an item not in the cart', () => {
// Updating the quantity of an item that's not in the cart should not throw an error
expect(() => cart.updateItemQuantity(itemB.id, 3)).not.toThrow();
});
});
使用測試案例生成代碼
當我們已經撰寫了測試案例後,現在可以開始實際撰寫 TypeScript 檔案 ShoppingCart.ts
。以下是我們實作的購物車類的程式碼:
// ShoppingCart.ts
import DiscountManager from "./DiscountManager";
import Item from "./Item";
class ShoppingCart {
private cart: { item: Item; quantity: number }[] = [];
private discountManager: DiscountManager; // Add a DiscountManager property
constructor(discountManager: DiscountManager) {
this.discountManager = discountManager;
}
addItem(item: Item, quantity: number) {
if (quantity <= 0) {
return; // Do nothing for non-positive quantities
}
const existingItem = this.cart.find(cartItem => cartItem.item.id === item.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.cart.push({ item, quantity });
}
}
removeItem(itemId: number) {
const itemIndex = this.cart.findIndex(cartItem => cartItem.item.id === itemId);
if (itemIndex !== -1) {
this.cart.splice(itemIndex, 1);
}
}
updateItemQuantity(itemId: number, quantity: number) {
const existingItem = this.cart.find(cartItem => cartItem.item.id === itemId);
if (existingItem) {
existingItem.quantity = quantity;
}
}
getTotalItems(): number {
return this.cart.reduce((total, cartItem) => total + cartItem.quantity, 0);
}
getItemQuantity(itemId: number): number {
const cartItem = this.cart.find(cartItem => cartItem.item.id === itemId);
return cartItem ? cartItem.quantity : 0;
}
getTotalPrice(promoCode?: string): number {
return this.cart.reduce((total, cartItem) => {
const { item, quantity } = cartItem;
const discountPercentage = promoCode ? this.discountManager.getDiscount(promoCode) : 0;
const discountedPrice = item.price * (1 - discountPercentage / 100); // Apply promo code discount
return total + discountedPrice * quantity;
}, 0);
}
clearCart() {
this.cart = [];
}
}
export { ShoppingCart, Item };
DiscountManager:
// DiscountManager.ts
class DiscountManager {
private discounts: Record<string, number> = {};
setDiscount(productId: string, discountPercentage: number) {
this.discounts[productId] = discountPercentage;
}
getDiscount(productId: string): number {
return this.discounts[productId] || 0;
}
}
export default DiscountManager;
擴展實際案例:處理負數數量
在我們的購物車實際案例中,我們已經為一個購物車應用寫了一些測試案例,以確保其正確性和穩定性。然而,一個重要的方面是確保購物車能夠正確處理不合法的操作,例如向購物車添加負數數量的商品。
為了應對這種情況,我們將添加一個新的測試案例,以確保購物車在這種情況下表現正確。以下是測試案例的描述和代碼示例:
it('should handle adding a negative quantity of items', () => {
// 假設購物車中已經有一些商品
cart.addItem(itemA, 2);
cart.addItem(itemB, 3);
// 在這個情境下,我們嘗試向購物車添加負數數量的商品
cart.addItem(itemA, -1);
// 我們期望購物車不應該發生變化,總數量仍然應為 5
expect(cart.getTotalItems()).toBe(5); // 商品 itemA 的數量仍然為 2
});
這個測試案例強調了購物車應該能夠適當地處理不合法的操作,例如嘗試添加負數數量的商品。通過這個測試,我們確保了購物車在處理這種情況時不會引發錯誤,並且總數量仍然保持不變。這有助於確保購物車的穩定性和可靠性,以提供良好的使用者體驗。
進階應用案例:促銷代碼折扣
在我們的購物車實際案例中,我們希望確保購物車能夠正確應用促銷代碼折扣,以提供使用者更有吸引力的價格。這個功能是一個常見的電子商務應用中的重要部分,它能夠吸引更多客戶並提高銷售量。
為了測試這個功能,我們將添加一個新的測試案例,以確保購物車能夠正確地應用促銷代碼的折扣。以下是測試案例的描述和代碼示例:
it('should apply promo code discount to all items', () => {
// 假設購物車中已經有一些商品
cart.addItem(itemA, 2);
cart.addItem(itemB, 3);
// 設置促銷代碼折扣(20% 折扣)使用 DiscountManager
discountManager.setDiscount('discount20Percentage', 20);
// 我們期望購物車計算總價格時不應該考慮折扣
expect(cart.getTotalPrice()).toBe(35);
// 使用促銷代碼折扣計算總價格,預期總價應為折扣後的價格
expect(cart.getTotalPrice('discount20Percentage')).toBe(28);
});
這個測試案例模擬了購物車中添加商品並使用促銷代碼折扣的情境。我們首先設置了一個促銷代碼折扣,然後通過 cart.getTotalPrice()
方法來計算原始總價格,確保它不考慮折扣。接著,我們使用促銷代碼折扣來計算總價格,預期總價應為折扣後的價格。
這個測試案例確保了促銷代碼折扣的正確應用,確保購物車能夠提供正確的價格,從而為使用者提供更有吸引力的購物體驗。
左右互搏術
當 ChatGPT 成為我的日常生產力工具之一,我像是發現了一把魔法的鑰匙,開啟了無限的奇妙之門。它在各個領域都展現了驚人的多才多藝,不論是生成文本、解答問題,還是創建內容,ChatGPT 不僅為我的工作帶來了極大的便利,也深刻地改變了我的生活。然而,正當我享受 ChatGPT 帶來的便捷時,一個不同尋常的想法浮上心頭:「能否讓 ChatGPT 成為測試驅動開發(TDD)的一部分?」
這讓我聯想起金庸筆下的雙手互搏之術:
雙手互搏(亦稱左右互搏)為金庸武俠小說《射鵰英雄傳》和《神鵰俠侶》中全真教的武功,「老頑童」周伯通(也就是《神鵰俠侶》裡的「中頑童」)被「東邪」黃藥師困於桃花島時所創。 維基百科
雖然看似截然不同的領域,但它們之間卻有一些意外的相似之處。雙手互搏是一種武功,強調「一心二用」,即同時處理兩個不同的事情。在武術中,這意味著將精力和技巧分別應用於左手和右手,實現全面的攻擊和防禦。這種分心二用的概念可以直接應用於 TDD,其中我們需要同時關注代碼的開發和測試,確保兩者相輔相成。
在 TDD 中,我們首先要設計測試用例,然後開始編寫代碼以滿足這些測試用例。這就要求我們將精力分散在兩個不同的方面:測試的設計和代碼的實現。就像雙手互搏中要同時處理左右手一樣,我們在 TDD 中要同時思考測試和代碼的兩個方面。
ChatGPT 還在測試過程中提供寶貴的反饋。當我們遇到錯誤時, ChatGPT 可以幫助我們深入分析問題並提供解決方案。例如,當測試失敗時, ChatGPT 可以生成關於可能的錯誤原因的建議,協助我們更迅速地定位並修復問題。這種即時的智慧反饋極大提高了測試和開發的效率。
這種同時處理測試和代碼的能力,就像雙手互搏中左右手的協同作戰一樣,能夠提高開發效率和代碼的質量。它讓我們可以更快速地識別和解決問題,同時確保軟體的穩定性和可靠性。在這個意想不到的交匯點上,我發現了一個全新的方法,將 ChatGPT 的潛力與測試驅動開發的原則相結合,帶來了更有力的開發工具和更優越的軟體品質。這是一個技術與武學的交融,為未來的軟體開發之路開辟了嶄新的可能性。
後記
現在,筆者身處於一個數碼遊牧的生活中,回顧 2023 年,這短短的四個月內,我就已經有機會到達四個不同的城市:新加坡、首爾、東京、台北。這種自由的生活方式不僅解放思維,更有機會重新詮釋人生的意義。儘管我可能賺取的收入較少,但我可以全身心地投入,運用技術手段幫助客戶實現他們的夢想。看到他們的夢想變成現實,我感到無比開心和滿足。
人生不僅僅是追求財富,更是關於追求有意義和充實的生活。這種自由和靈活性讓我更能夠用自己的方式貢獻社會,並實現自己的價值觀。同時,我也學到了許多關於不同文化、不同人的寶貴經驗,這讓我變得更加開放和包容。
未來充滿了無限可能性,我期待著更多的冒險和挑戰,以及更多的成長和收穫。這就是我所追求的生活,一個自由而有意義的生活。