🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

在前端圈,單元測試幾乎是一個非常頭疼的話題。履歷上不寫熟悉單元測試,都不好意思跟人打招呼了。CI/CD流程裡,要是沒有一個 test 的階段,就好像這個專案不夠專業。而那綠色的“Coverage: 95%”,也常常被當作專案質量的黃金標準,成為許多團隊KPI的一部分。

但在我帶團隊的這幾年,尤其是在經歷了幾個專案因為高覆蓋率的單元測試而舉步維艱之後,我越來越傾向於一個結論:

我們前端領域裡,大部分團隊寫的大部分所謂單元測試,純屬自欺欺人。

在你說我反測試之前,請允許我加一個前提:我反對的,不是測試本身,而是那種為了覆蓋率而寫的、脫離用戶實際場景的、脆弱不堪的、以測試實現細節為樂的單元測試


單元測試,長什麼樣?

我們先來看幾個我從過往專案中挖出來的、非常典型的測試案例。

假設我們有一個簡單的計數器元件:

// Counter.jsx
class Counter extends React.Component {
  state = { count: 0 };
  increment = () => this.setState({ count: this.state.count + 1 });
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

然後,一份看起來正確的單元測試:

// Counter.test.js
it('should increment count when button is clicked', () => {
  const wrapper = shallow(<Counter />);
  wrapper.find('button').simulate('click');
  // 重點:測試元件的 state
  expect(wrapper.state('count')).toBe(1); 
});

為什麼說這是自欺欺人?

這個測試,和元件的內部實現(this.state.count)牢牢地綁在一起。明天,我如果用Hooks重構這個元件,把state換成了useState,元件給用戶看的功能完全沒變,但這個測試百分之百會掛掉。

Mock測試

image.png

我們經常會寫一些依賴其他模組或Hook的元件。於是,測試程式碼就變成了:

// 一個依賴了 useAuth 和 useApi 的元件
jest.mock('./hooks/useAuth');
jest.mock('./api/user');

it('should display user name when logged in', () => {
  // 模擬 useAuth 返回已登錄
  useAuth.mockReturnValue({ isLoggedIn: true, user: { name: 'John' } });
  // 模擬 api 調用
  userApi.getInfo.mockResolvedValue({ success: true, data: ... });

  render(<UserProfile />);
  // ...斷言
});

當你的測試文件裡,jest.mock的程式碼比斷言的程式碼還多時,你就應該警惕了。我們到底是在測試我們的元件邏輯,還是在測試我們Mock的能力?這種測試,一旦元件的依賴關係發生微小的變化,整個測試文件可能都要重寫。


這些寫法帶來了什麼惡果?

這些測試累積起來,會給團隊帶來災難性的後果。

團隊看著CI上那95%的覆蓋率,覺得高枕無憂。但實際上,這些測試只保證了“當A函數的輸入是1時,返回值是2”,卻完全保證不了“當用戶在頁面上完成A、B、C三個操作後,頁面能正確跳轉”。高覆蓋率,不等於高質量。

對重構的巨大阻力

這才是最致命的。

開發者不敢動那些老程式碼,不是因為怕改出Bug,而是因為怕改完之後,要花雙倍的時間去修復那些因為實現細節變更而大面積失敗的、脆弱的單元測試。這極大地扼殺了專案的迭代和優化動力。

浪費寶貴的開發時間

團隊投入大量時間,去編寫和維護這些弱雞😒的測試,僅僅是為了讓CI上的那個數字好看一點。這些時間,本可以用來做更有價值的事情,比如寫更重要的整合測試,或者直接去優化產品功能。


我的方法論:像用戶一樣去測試

聊了這麼多問題,那到底什麼樣的測試,才不是自欺欺人?

我的核心觀點是:前端測試的重心,應該從單元測試,向整合測試和端到端測試傾斜。

也就是說:測試你的軟體,而不是你的實現細節。

我們把第一個計數器的例子,用這種思想重新寫一遍(使用React Testing Library):

// Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react';

it('should display the updated count after clicking the button', () => {
  render(<Counter />);

  // 找到螢幕上的按鈕並點擊它,模擬用戶的真實操作
  const button = screen.getByRole('button', { name: /increment/i });
  fireEvent.click(button);

  // 驗證用戶在螢幕上看到的結果,而不是內部狀態
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

這才是一個有價值的測試!

你看,這個測試,完全不關心 count 這個狀態是存在 classstate 裡,還是 useState 裡。它只關心一件事:“當用戶點擊了那個叫 Increment 的按鈕後,他是不是能在螢幕上看到 Count: 1 這段文字?

無論你未來怎麼重構 Counter 元件,只要它的用戶行為沒有變,這個測試就不會失敗。這才是我們需要的、堅固的、能給予我們信心的測試。


最後的最後

我們應該把大部分精力,投入到模擬用戶真實操作路徑的測試上,確保應用的功能是正確的。而對於那些純粹的、無副作用的工具函數(比如一個 formatDate 函數),單元測試依然是必要的,但這在我們的程式庫裡,只占很小一部分。

測試不是目的,保證軟體能正常工作才是。

是時候審視一下我們專案裡的 *.test.js 文件了,然後問問自己:

我寫的這些測試,到底是在保證質量? 還是在自我安慰,看起來很牛皮?

你們覺得呢?🤔


原文出處:https://juejin.cn/post/7552350928220078120


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬9   ❤️3
494
🥈
我愛JS
📝3   💬8   ❤️7
169
🥉
AppleLily
📝1   💬4   ❤️1
65
#4
xxuan
💬1  
3
#5
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付