前端單元測(cè)試,更進(jìn)一步

前端測(cè)試@2022
如果從 2014 年 Jest 的第一個(gè)版本發(fā)布開(kāi)始計(jì)算,前端開(kāi)發(fā)領(lǐng)域工程化的單元測(cè)試能力已經(jīng)發(fā)展了八年有余。

Jest 集成了 Jasmine 等以往各種被證明有效的單元測(cè)試框架和斷言等工具,也可以用來(lái)完成包含外部接口服務(wù)的集成測(cè)試等。
最近幾年熱門(mén)的 vite 打包工具配套的 vitest,也是完全兼容 Jest 工具棧的;除了本身相比于 Jest 帶來(lái)了比較大的性能提升之外,vitest 還提供了更好的 ESM 等支持。一般也用 @testing-library 來(lái)搭配 vitest,提供 DOM 等核心測(cè)試能力。

Storybook 則在瀏覽器環(huán)境中,為 UI 組件的單獨(dú)編寫(xiě)和測(cè)試提供了可視化的、可交互的、與具體業(yè)務(wù)項(xiàng)目無(wú)關(guān)的單獨(dú)運(yùn)行環(huán)境;無(wú)論是 web 項(xiàng)目還是混合式的桌面應(yīng)用,都可以不理會(huì)繁復(fù)的項(xiàng)目配置和依賴,把組件級(jí)別的開(kāi)發(fā)在 Storybook 中快速完成。

在測(cè)試分層金字塔模型中,最終還需要立足真實(shí)業(yè)務(wù)項(xiàng)目的 UI 測(cè)試,也就是終端用戶(或 QA 測(cè)試人員)到終端設(shè)備的 E2E(end to end) 測(cè)試。
一般所說(shuō)的 自動(dòng)化測(cè)試 指的大都是對(duì)于 E2E 測(cè)試的自動(dòng)化。Selenium 是自動(dòng)化測(cè)試的常用工具,但新興的 Playwright 顯然得到了越來(lái)越多的青睞;后者還能更好地支持 electron 等桌面開(kāi)發(fā)項(xiàng)目。
play 一下
在開(kāi)發(fā)實(shí)踐中對(duì)比幾種測(cè)試,Jest/vitest 單元測(cè)試易于開(kāi)發(fā)人員編寫(xiě),但其運(yùn)行在命令行下,不夠直觀;而 Storybook 展示直觀,卻大部分只能靠開(kāi)發(fā)者人工檢查其有效性,由于無(wú)法集成到 pre-commit 等開(kāi)發(fā)流程中,也容易重蹈早期 Jasmine 等基于瀏覽器頁(yè)面單測(cè)用例的覆轍 -- 編寫(xiě)簡(jiǎn)單但很容易過(guò)時(shí)失效。
較新版本的 Storybook 中引入了 交互式測(cè)試(Interaction Test) 的概念,用法也極為簡(jiǎn)單,只需要為既有的 UI 用例編寫(xiě)一個(gè) play() 函數(shù) 就可以了。
// LoginForm.stories.js|jsx
import React from 'react';
import { within, userEvent } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { LoginForm } from './LoginForm';
export default {
title: 'Form',
component: LoginForm,
};
const Template = (args) => <LoginForm {...args} />;
export const EmptyForm = Template.bind({});
export const FilledForm = Template.bind({});
// 為具名用例增加 play()
FilledForm.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
// 復(fù)用單測(cè)中的 testing-library 庫(kù)模擬用戶行為
await userEvent.type(canvas.getByTestId('email'), '[email protected]');
await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
await userEvent.click(canvas.getByRole('button'));
// 直接用 jest/vitest 等提供的斷言函數(shù)
await expect(
canvas.getByText(
'Everything is perfect. Your account is ready and we should probably get you started!'
)
).toBeInTheDocument();
};
類似單測(cè)在命令行中的紅綠結(jié)果,交互式測(cè)試的每個(gè)步驟、其成功失敗,都會(huì)顯示在相應(yīng)的面板中:

復(fù)用測(cè)試用例
不難發(fā)現(xiàn),工具棧相同、寫(xiě)法無(wú)異,play 函數(shù)對(duì)于習(xí)慣了寫(xiě)單元測(cè)試的前端開(kāi)發(fā)者來(lái)說(shuō)并不陌生,或者可以說(shuō)是零門(mén)檻的,play 函數(shù)中的代碼就是標(biāo)準(zhǔn)的單測(cè)代碼。那么我們也沒(méi)有任何理由讓這部分測(cè)試代碼游離在覆蓋率統(tǒng)計(jì)之外,或是再去單測(cè)中編寫(xiě)重復(fù)的代碼了。
需要做的也非常簡(jiǎn)單,直接在單測(cè)中 import 后 play 就是了:
// foo.spec.jsx
import { render } from '@testing-library/react';
import { FooUISpec } from '../Foo.stories';
it('Checks by storybook', async () => {
const { container } = render(<FooUISpec />);
await FooUISpec.play({ canvasElement: container });
});
總結(jié)
現(xiàn)在,我們可以讓 Storybook 和單元測(cè)試分享測(cè)試用例,甚至可以在 Playwright 中調(diào)用 Storybook 服務(wù)后再編寫(xiě)自動(dòng)化測(cè)試 -- 后者這里不展開(kāi)討論了;總之,測(cè)試工具的發(fā)展,給了前端開(kāi)發(fā)者更直觀編寫(xiě)測(cè)試用例的手段,最終也更好地保證了前端項(xiàng)目的開(kāi)發(fā)質(zhì)量,以及代碼編寫(xiě)的合理性。
