iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0
Software Development

全端實戰心法:小團隊的產品開發大小事系列 第 21

單元測試(三):前端有哪些重點 Unit Tests 要寫?

  • 分享至 

  • xImage
  •  

前兩講聊到 Unit Tests 要寫什麼用 Stub 和 Mock 來處理 Side Effect 的情況。今天則讓我們來看看前端和後端有什麼重點的 Unit Tests 可以測試,畢竟寫測試也是需要花時間的,我們不太可能把所有的程式碼都覆蓋上 Unit Tests,在時間有限的情況下應該要挑重點來做。

工具類 Function

我認為前後端都有的工具類(Utility)Function 最適合做單元測試,因為大部分的工具類 Function 都不需要特別考慮 Side Effect,有單純的輸入以及輸出。

如果沒有 Side Effect,就意味著我們不需要 Stub 及 Mock 這兩招來應對外部環境的影響,可以將心思都花在輸入的資料有哪些、期望的輸出是什麼。如果有 Side Effect,特別是在用到 Mock 時,撰寫測項的時間成本就高得多,因為需要考慮如何和 Mock 的環境互動。

因此,實務上我會盡可能的把所謂工具類的 Function 從有 Side Effect 的大 Function 裡面拆出來,特別針對此做單元測試。

那麼怎麼樣的 Function 是工具類的呢?

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

function calculateAverage(numbers) {
  return numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
}

function convertSpeed(speedInMs, targetUnit) {
  switch (targetUnit) {
    case 'mph':
      return speedInMs * 2.23694;
    case 'kph':
      return speedInMs * 3.6;
    default:
      throw new Error('Invalid target unit');
  }
}

以上如資料轉換、計算類,包含明確條件(if else / switch case),但是輸入和輸出都是可控、可預期的,就非常適合寫 Unit Tests。

以上 Function 的邏輯都算是簡單的,而 Unit Testing 更適用在邏輯再複雜一些,需要花點心思才能釐清的 Function 上,寫完了不但可以預防之後修改程式碼不小心改壞,也能夠當作理解這個 Function 的使用手冊。

前端的 Unit Tests

除了適用前後端的工具類 Function,如果聚焦在前端上,我認為 UI 的 StateEvent 類最適合寫 Unit Tests。

其中 Event 類指的是 User 和 UI 的互動,例如點擊 Button、拖曳物件等等的行為,輸入就是用戶的的動作,而輸出則是產生的 Event。

至於 UI 的 State 則是我接下來舉的例子中要聊到的:

例如一個複雜的表單,其中的某個元件狀態會根據其他元件而被影響,我們就可以撰寫 Unit Tests 來羅列輸入的元件狀態,可以怎麼影響到目標的元件狀態。

用一個簡化的例子來說,我們有一個可以提交意見的表單,只有一個輸入的 Input(Textarea)和提交的 Button。

如果 Input 沒有內容,則提交 Button 就不能按;而且輸入也不能超過 30 個字,一旦超過則提交 Button 也會被 Disable。

Feedback 表單及 Button 狀態示意圖
*Feedback 表單及 Button 狀態示意圖

以下是用 React 所實作的 FeedbackForm Component,其中狀態轉換只用簡單的邏輯來判斷 Input 的字數,進而改變 Button 是否 Disable。

import React, { useState } from 'react';

function FeedbackForm() {
  const [inputValue, setInputValue] = useState('');
  const isButtonDisabled = inputValue.length === 0 || inputValue.length > 30;
  
  return (
    <div>
      <textarea value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
      <button disabled={isButtonDisabled}></button>
    </div>
  );
}

export default FeedbackForm;

那麼,我們要怎麼寫單元測試呢?

我們可以 Render 一個 FeedbackForm,將 InputElement 塞入不同的值,參照前兩講提過的 Boundary Testing,將一些邊界的數值納入測試。

由於 InputElement 沒有任何內容和超過 30 字的情況會使得 Button Disabled,這邊的 Boundaries 就可以丟入 0, 1, 30, 31 的邊界值來測試。

test.each([
  { input: '', expectedDisabled: true},
  { input: 'a', expectedDisabled: false},
  { input: 'a'.repeat(30), expectedDisabled: false},
  { input: 'a'.repeat(31), expectedDisabled: true},
])('With input "$input", button disabled state should be $expectedDisabled', ({input, expectedDisabled}) => {
  render(<FeedbackForm />);
  const inputElement = screen.getByRole('textbox');
  const buttonElement = screen.getByRole('button');

  fireEvent.change(inputElement, { target: { value: input } });
  expect(buttonElement).toBeDisabled(expectedDisabled);
});

如此一來,我們就將 Button 因為不同輸入而有不同狀態的測試完成了。


上一篇
單元測試(二):應對 Side Effect 的 Stub 及 Mock
下一篇
整合測試:確保基本功能不會被改壞
系列文
全端實戰心法:小團隊的產品開發大小事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言