iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0
Modern Web

與 AI 一起開發 Side Project 吧!系列 第 19

Day19 — 穩如泰山 | 測試也是需要整理維護的,跟 AI 一起整理測試

  • 分享至 

  • xImage
  •  

前言

AI 代工替我們寫的測試程式碼,已經涵蓋了目前完成的使用者故事的測試項目。分別是這兩項:

  • 作為一個 [重度記帳軟體的使用者],我想要 [單手完成記帳操作],以便 [節省記帳時間且避免忘了記帳]。
  • 作為一個 [重度記帳軟體的使用者],我想要 [快速編輯 / 刪除記帳],以便 [節省記帳修改時間]。

自動化測試 — 產品的第一個用戶

產品驗收小幫手

除了我自己擔任 QA(品保員) 實際操作覺得 OK,驗證程式的功能無誤,測試是另一個「更為可靠」的 QA 。不只驗收的速度快,幾毫秒的眨眼之間即完成,而且可以保證每次驗收的情境與流程都不變。我們可能測一測還會忘記其中幾步,或是搞錯什麼步驟,而自動化測試幾乎不會有這個問題

且剛好今天的記帳 App 是先打造前端的操作流程,才有 UI 可以讓使用者操作,若今天打造的是後端等沒有可操作畫面的服務,通常測試就會是程式功能的「第一線用戶」,得先讓程式至少通過理想情境(Happy Path)的測試,才有信心說程式功能已經完成了。

測試相當重要,是我們產品最忠實,不離不棄的第一個用戶。

測試也是產品的一部分

驗收項目的測試是有了,但似乎不夠好懂,但你可能會疑問,照剛剛所說,測試只不過是「程式的用戶」,應該不算是終端產品的一部份吧?又不會拿給客戶用,有必要做重構嗎?

但其實還是要,測試是用來服務我們「開發者」用的,是功能的一部分,當然也就是產品的一部分。日後功能做了修改,測試因而壞掉之後,還是要回頭去改「測試的程式碼」,改程式當然也要看程式,所以囉,當然測試程式要好懂一點才好呀。

因此,測試也要重構,跟著產品的迭代同進同出。

整理測試也是有技巧的

BDD 做做看

先前是快速請 AI 幫我產測試,但回頭來看,有沒有「更科學可靠」的方式,從需求層面化身為驗收條件,接著變成測試項目呢?如此一來,測試應該就能相對好懂一點,知道每個測試相對應的需求是什麼。

自然是有辦法,這邊先簡單參考 BDD 的 Specifications by Example 作為驗收案例的一個方法。

而什麼是 Specifications by Example ?直翻的話是「規格範例」,嗯…聽君一席話,如聽一席話,有聽沒有懂 🤔…

Specifications by Example

將使用者故事(需求範例)化身為驗收條件(規格),Specifications by Example 做的就是把業務需求,直接化為規格。如此的好處是,需求是什麼樣的,測試就會是什麼樣,那如果測試「通過」,代表需求也是通過的了。

而業務需求的寫法,如同使用者故事那樣,有一套「模板」可以參考如何撰寫,以下是其中 3 個常有的必要條件描述:

  • *Given:當…情境的時候
  • *When:在…行為觸發之後
  • *Then:就會發生…事情

那我們可以拿原本的測試,試著練習看看。就拿最經典的「新增一筆記帳金額」這個測試來 try try

it('adds new item to history when category is selected and confirms', () => {
	  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')); 
	  expect(screen.getByText('飲食')).toBeInTheDocument();
	  expect(screen.getAllByText('$10')).toHaveLength(2); 
});

原本程式碼這樣看起來有點難讀…,先直覺地按照邏輯分段一下,並暫時加個註解,以方便大家理解。

好了,調整過後長這樣:

it('adds new item to history when category is selected and confirms', () => {
	  // 渲染
	  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')); 
	  
	  // 確認種類和金額正確
	  expect(screen.getByText('飲食')).toBeInTheDocument();
	  expect(screen.getAllByText('$10')).toHaveLength(2); 
});

這樣看起來清楚多了,每段程式碼做的事,好理解多了 👍。而且好像可以…套用上面提到的 given, when 和 then 來把各段抽成方法囉!? 💡

首先是 given ,為「前置條件」,通常是指準備資料,像是需要把後端拿回來的資料做 mocking,但今天沒有這樣的情況,因此不需要。

而 when ,則是指「某個行為被觸發」了。像是「渲染」組件 renderApp(<App />),就是「當渲染了 App 畫面」。所以可以先把部分抽成 whenRender() 的方法,一次先做一步驟。

function whenRender() {
  render(<App />);
}

it('adds new item to history when category is selected and confirms', () => {
    whenRender()

		//...

重構之後,雖然只有改一個小東西,記得再跑一下測試,看看有沒有把測試改壞(這是一個習慣,或是你也可以用 watch 模式)。

看起來沒問題,看看還有沒有 when 的條件,往下看 fireEvent.click(…) ,翻成白話文就是「 when 點擊了包含 … 的元素」,觸發了這個元素的點擊,那確實是 when 沒錯。

於是乎,將 fireEvent 那幾組抽成方法,大概是如下這樣


function whenInputNumber(number: number) {
  number.toString().split('').forEach(digit => {
    fireEvent.click(screen.getByText(digit));
  })
}

function whenClickOK() {
  fireEvent.click(screen.getByText('OK'));
}

function whenSelectCategory(category: string) {
  fireEvent.click(screen.getByText(category));
}

it('adds new item to history when category is selected and confirms', () => {
  whenRender()

  whenInputNumber(10);
  whenClickOK();
  
  whenSelectCategory('飲食');
  whenClickOK();
  
  expect(screen.getByText('飲食')).toBeInTheDocument();
  expect(screen.getAllByText('$10')).toHaveLength(2); 
});

最後,剩下 then 的條件,也就是「預期發生什麼事」,大家到這應該看得很明白了,沒錯,就是 expect(某些套件是用 assert…)這個地方。於是乎,也是行以「抽方法」。

我是用 Cursor Tab 提示 AI 我要抽方法。先輸入 thenCategoryShouldHave 在 whenClickOK() 的下方,AI 就知道我是要把該區塊抽成方法,等 Cursor IDE 的 auto-complete 提示跳出來,接續按下 Tab 就完成修改囉。

當然你也可以走「正規的」方法,請 IDE 幫我們 extract method,這樣也蠻快的。

//...

function thenCategoryShouldHave(category: string) {
  expect(screen.getByText(category)).toBeInTheDocument();
}

function thenAmountShouldBe(amount: string) {
  expect(screen.getAllByText(amount)).toHaveLength(2);
}

it('adds new item to history when category is selected and confirms', () => {
  whenRender()

  whenInputNumber(10);
  whenClickOK();
  
  whenSelectCategory('飲食');
  whenClickOK();
  
  thenCategoryShouldHave('飲食');
  thenAmountShouldBe('$10');
});

調整寫法,以除後患

如今總算把其中一個測試重構,改得更像人話一點。一次先調整一個測試,這樣壞了就知道是哪裡沒調整好。因此,今天就先舉例到這打住,如果忍不住想試試的小夥伴,可以先自己練習看看 🙂 

接下來會延續套用這個方法,把其他測試寫得更清楚好懂喔!

結語

測試也要易讀好懂

不是說寫完「保護程式邏輯」就好了,測試一大用處也是作為程式文件留存,等之後回頭看這部分的程式時,就先看測試,請 AI 幫我「說故事」,會比直接看程式邏輯要來得直觀許多。

最後,順帶一提,用了 BDD 的一些方法,讓測試更好懂。BDD 不一定要搭配 TDD,而我們也確實沒有這麼做。

REF


上一篇
Day18 — 穩如泰山 | 極速產單元測試,如何辦到?
下一篇
Day20 — 穩如泰山 | 急速產單元測試 Part2,優化指令產出更好的測試!
系列文
與 AI 一起開發 Side Project 吧!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言