跳至主要内容

[Day 26] 快照測試(Snapshot Testing)是什麼?什麼時間適合使用?

Snapshot Testing

快照測試第一時間聽起來好像是會「幫我們的畫面做一個快照,紀錄下來當時的畫面」,但這樣的說法對也不對,今天就讓我們來了解一下所謂的快照測試(Snapshot Testing)。

快照測試的原理

快照測試(Snapshot Testing)是由 Jest 提供的一種測試方式,它的原理是在進行測試時,Jest 會把開發者指定的元素轉譯(render)成 DOM 後保存成一個檔案紀錄下來,一般這個檔案的副檔名會以 .snap 結尾。

快照測試的原理蠻單純的,它就是在執行測試時,比對當前的快照檔內容和前一次的快照檔是否相同,如果是第一次執行還沒有前一次的快照可以比較的話,就會自動生成一份新的快照檔。如果當前快照檔和前一次的快照檔內容不同的話,測試就不會通過,也就是說,只要這次轉譯出來的 DOM 和前一次紀錄下來的 DOM 有任何不同,測試都不會通過。

舉例來說,如果現在有一個 React 元件 Button

// Button.tsx
const Button = () => {
return <button>Click me</button>;
};

export default Button;

在第一次執行快照測試時,Jest 就會自動建立一個 __snapshots__ 資料夾,且裡面有一隻名為 Button.test.tsx.snap 的檔案,而它的快照檔內容則會長像這樣:

// __snapshots_/Button.test.tsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`create snapshot test for the Button 1`] = `
<button>
Click me
</button>
`;

現在如果把原本 Button 元件中的文字從 Click me 改成 Click here 的話,測試就會失敗:

Snapshot Testing

也就是說,只要「這次 render 出來的 DOM 和前一次紀錄的 DOM 不同」測試就不會通過。

為什麼需要快照測試(Snapshot Testing)

從快照測試的原理可以知道,只要畫面和過去有任何不同,測試就會報錯,這樣聽起來,幾乎只要每次程式有改動,測試結果就一定會失敗(failed),這麼說起來快照測試到底有什麼好處呢?

快照測試最重要的目的是提醒開發者:「這裡有不同喔!這個不同是你預期該有的不同嗎?

你可以想像有些時候,特別是一些單純的 UI 元件,花許多時間去寫這個元件的 Unit Test 可能 CP 值不太高,因為在寫測試時你需要先把該元件 render 出來,接著透過一些 selector 去選到該元素,然後透過 assert 確保該元素真的有 render 出來,並且在 assert 元素裡的內容是相同的。舉例來說,剛剛那個簡單的 Button 元素,寫成 Unit Test 會像這樣:

test('button can render successfully with correct text', () => {
render(<Button />);
const linkElement = screen.getByText(/Click me/i);
expect(linkElement).toBeInTheDocument();
});

但一般的 UI 都不會這麼簡單,如果要一個個選出來再 assert 的話,相當麻煩。

這時有快照測試的話就相當方便,只需在開發好元件,且確認 UI 是正確的後,為它拍一個快照紀錄下來,後續只要這個 UI 的內容有變更,測試在執行的時候都會 failed,以提示開發者:「這裡有不同喔!請留意!」。

更新快照

當開發者發現測試錯誤時,就要去檢視這個差異是預期的嗎?以剛剛來說,因為我們把原本 Button 元件的文字內容從 Click me 改成 Click here,所以現在這個錯誤是合理且符合預期的:

Snapshot Testing

這時候開發者要做的就是更新快照,在 Jest 的 watch mode 中,可以看到更新快照的選項:

Snapshot Testing

可以透過 u(更新所有有問題的快照) 或 i(逐一選擇要更新的快照)來更新快照,更新完後,測試就會通過,同時原本的快照檔也會更新成當前 DOM 的樣子。

原則上這些快照檔也都需要一起透過 git 來追蹤,因此在發 PR 的時候,reviewer 也可以很容易的掌握 UI 上有變更的部分:

Snapshot Testing

撰寫快照測試

在瞭解了快照測試的概念後,來看一下剛剛 Button 元件的快照檔是如何產生(這裡以 React 元件為例):

// Button.test.tsx
import Button from './Button';
import { create } from 'react-test-renderer';

test('<Button /> should render click here', () => {
const tree = create(<Button />).toJSON();
expect(tree).toMatchSnapshot();
});

就是這麼容易,首先載入需要被快照的元件,並使用 React 官方提供的 react-test-renderer 套件(需要自行 npm install)。接著透過 createtoJSON() 將 Button 元件變成一個可以被保存下來的物件,最後呼叫 toMatchSnapshot() 這個方法,Jest 就會幫我們建立以及比對 .snap 檔。

快照測試的使用時機及注意事項

從上面的說明中可以知道,快照測試這種一旦畫面有變更就要提示的特性,比較適合用在 UI 元件/函式庫,因為這些 UI 元件通常寫好後被變更的機會較小,且 UI 本身的正確性是它的重點;快照測試無法檢測元件的邏輯和模擬使用者的互動,例如點了按鈕下拉選單要展開這類的,因此如果有這個需求,需要輔以其他單元測試或整個測試,但至少快照測試可以確保 UI 的穩定性。

註:快照測試雖然無法直接測試使用者互動的情況,但可以透過 props 的方式來執行某些 event listeners 來促使畫面改變,詳細的方式可以參考 snapshot-testing @ Jest

看起來快照測試著重的是 UI,那為什麼今天的開頭提到「快照測試是幫我們的畫面做一個快照,紀錄下來當時的畫面」這種說法對也不對呢?

原因在於快照測試紀錄下來的是 DOM 本身,而不是使用者真正看到的畫面,快照測試不像 visual regression testing 這種會實際以畫面截圖進行像素比對所進行的測試,因此精確來說,快照測試紀錄的只是 DOM,而不是真實的畫面

最後,快照測試所產生的 snap 檔記得要被 git 所追蹤和紀錄,如此才能正確測試 DOM 有無變更,也能更清楚每次 DOM 的變化,因此記得不要把它們給 ignore 了。

參考資料