上一次我們只調整了一個測試,花上了不少時間,因為還在摸索如何調整。畢竟萬事起頭難,第一次總是最讓人勸退。
不過有了上次經驗之後,再調整第二個、第三個…,應該會快上不少,有了整理過的測試,還有流程可以做參考,想必會快上許多吧!
標準的測試「模板」,上次已經藉由 BDD 整理好了,再為大家複習一下,大概長這樣:
it('adds new item to history when category is selected and confirms', () => {
whenRender()
whenInputNumber(10);
whenClickOK();
whenSelectCategory('飲食');
whenClickOK();
thenCategoryShouldHave('飲食');
thenAmountShouldBe('$10');
});
由 when(行為) 和 then(預期)兩大部分所組成,比較像是人類講的白話文,而不是程式實作細節那般文謅謅。那試著下指令告訴 AI,參考這個「模板」,把另一個測試也調整看看囉。
先調整這個測試:
it('deletes item from history when delete button is clicked', async () => {
render(<App />);
fireEvent.click(screen.getByText('1'));
fireEvent.click(screen.getByText('0'));
fireEvent.click(screen.getByText('OK'));
fireEvent.click(screen.getByText('飲食'));
fireEvent.click(screen.getByText('OK'));
const deleteButton = screen.getByText('刪除');
fireEvent.click(deleteButton);
expect(screen.queryByText('飲食')).not.toBeInTheDocument();
expect(screen.queryByText('$10')).not.toBeInTheDocument();
});
給 AI 以下指令,請他幫我照著之前寫過的測試來重構:
請參考此檔案中的 adds new item to history when category is selected and confirms BDD 寫法,重構 deletes item from history when delete button is clicked 這個測試
看了生成的結果, AI 確實有幫我把「已經有 function 的地方」做了重構,但其他沒提到的,就沒有做 ,還真是老實 😂
所以還要補充一下 follow up 指令
另外,「刪除項目」這部分抽成方法,驗證「已被刪除」也抽成方法
最後終於好了,該抽的方法有幫我抽起來
//...
function whenDeleteItem() {
const deleteButton = screen.getByText('刪除');
fireEvent.click(deleteButton);
}
function thenItemShouldBeDeleted(item: string, amount: string) {
expect(screen.queryByText(item)).not.toBeInTheDocument();
expect(screen.queryByText(amount)).not.toBeInTheDocument();
}
it('deletes item from history when delete button is clicked', async () => {
whenRender();
whenInputNumber(10);
whenClickOK();
whenSelectCategory('飲食');
whenClickOK();
thenCategoryShouldHave('飲食');
thenAmountShouldBe('$10');
whenDeleteItem();
thenItemShouldBeDeleted('飲食', '$10');
});
有了兩個很像,但又不太一樣的測試,AI 應該可以參考寫法,幫我重構得很好吧? 於是乎,選擇了 claude-3.5-sonnet 模型,並提供以下指令:
請幫我依照 'adds new item to history when category is selected and confirms' 和 'deletes item from history when delete button is clicked' 的寫法(BDD 開發),
將其他「所有測試」都重構成如此寫法,謝謝
眨眼間的功夫,幫我把整份測試都重構了一輪,也很貼心地幫我調整了 function 的順序 🥺
function whenRender() {
render(<App />);
}
function whenInputNumber(number: number) {
number.toString().split('').forEach(digit => {
fireEvent.click(screen.getByText(digit));
});
}
//...
describe('AccountingApp', () => {
it('renders two components with total amount as $0', () => {
whenRender();
thenAmountShouldBe('$0');
});
it('allows input of numbers and updates the display', () => {
whenRender();
whenInputNumber(123);
thenElementShouldExist('$123');
});
it('clears the input when AC button is clicked', () => {
whenRender();
whenInputNumber(12);
whenClickButton('AC');
thenAmountShouldBe('$0');
});
it('removes last digit when backspace button is clicked', () => {
whenRender();
whenInputNumber(123);
whenClickButton('⌫');
thenElementShouldExist('$12');
});
//...
就此,重構差不多可以算大功告成了,如果還想要做得更好,是可以將相關的邏輯,以空行隔開,像是如下所示,這樣會更清楚一點喔。
// before
it('adds new item to history when category is selected and confirms', () => {
whenRender();
whenInputNumber(10);
whenClickOK();
whenSelectCategory('飲食');
whenClickOK();
thenCategoryShouldHave('飲食');
thenAmountShouldBe('$10');
});
// after
it('adds new item to history when category is selected and confirms', () => {
whenRender();
whenInputNumber(10);
whenClickOK();
whenSelectCategory('飲食');
whenClickOK();
thenCategoryShouldHave('飲食');
thenAmountShouldBe('$10');
});
這次演示的測試重構,是由下而上做重構。是等具體的測試實作都寫好之後,再來把類似或重複的邏輯做整理,將測試重構得更好理解,結構變得更簡潔。將最具體的
測試細節,重構為抽象的
概念測試行為。
下次要介紹給大家的是另外一個寫法,試試看「由上而下」來寫測試,先寫抽象的
概念測試行為,再來才是把具體的
測試細節給補上。