前端测试一共有哪几种?
前言
哈嘍,大家好,我是海怪。
最近有不少朋友找到我聊了聊測試相關的內容,發現他們對測試的分類有些迷茫。實際上測試一共就 3 種:E2E,集成,單測,其它的功能測試、UI 測試、界面測試只是它們中里面的一種。
Kent C. Dodds
在這篇文章 《Static vs Unit vs Integration vs E2E Testing for Frontend Apps》
也聊到了這 3 種測試的對比和區別,除此之外,還聊到它們各自的適用場景,應該對還在迷茫中的同學有所幫助。所以今天把這篇文章分享給大家~
翻譯中會盡量用更地道的語言,這也意味著會給原文加一層 Buf,想看原文的可點擊 這里。
正片開始
J.B. Rainsberger 在我的采訪里說了一個我很喜歡的比喻:
你可以把油漆扔到墻上,最終你可能會涂到大部分的墻壁,但除非你用刷子來刷墻,否則你永遠不會刷到角落。
我喜歡用它來類比測試,因為做測試就跟刷墻一樣,在開始之前要選擇正確的策略。你會用小刷頭來刷墻么?當然不會。那會花很長時間,而且效果也不均勻。
那你會用滾筒來刷所有東西么?比如拿它來刷兩百年前你的曾曾祖母從別的地方帶來的豪華家具?絕對不會。不同的刷子適用不同的場景,測試也是如此。
這就是為什么我會構建這個 測試模型。
在這個模型里,有 4 種測試分類:
- 端對端測試:利用一個很像用戶行為的機器人來和 App 交互,并驗證功能是否正常。有時也會說 “功能測試” 或 E2E。
- 集成測試:驗證多個單元是否能協調共同工作。
- 單元測試:驗證單獨隔離的部分是否正常工作。
- 靜態測試:捕獲寫代碼時的錯別字和類型錯誤
在這個模型里,每個測試分類的大小和你在測試時的關注度呈正相關(通常來說)。下面我來深入地聊聊這幾種測試類型的區別、含義、以及如何對它們做優化。
測試類型
讓我們從上往下看幾個這類測試的例子:
端對端測試
一般來說,它會跑完整個應用(前端 + 后端),這樣的測試會像真實用戶那樣和應用進行交互。下面的例子是用 Cypresss 來實現的:
import {generate} from 'todo-test-utils'describe('todo app', () => {it('should work for a typical user', () => {const user = generate.user()const todo = generate.todo()// 這里我們會走通整個注冊流程// 我一般只會寫一個測試來這么做// 剩下的測試則會通過直接發 HTTP 請求來實現注冊功能// 這樣我們就可以跳過這個注冊表單的交互過程了cy.visitApp()cy.findByText(/register/i).click()cy.findByLabelText(/username/i).type(user.username)cy.findByLabelText(/password/i).type(user.password)cy.findByText(/login/i).click()cy.findByLabelText(/add todo/i).type(todo.description).type('{enter}')cy.findByTestId('todo-0').should('have.value', todo.description)cy.findByLabelText('complete').click()cy.findByTestId('todo-0').should('have.class', 'complete')// 等等...// 我的 E2E 測試一般都會寫像真實用戶那樣// 有時會寫得非常長}) })集成測試
集成測試背后的思想就是盡可能少的 Mock。我一般只會 Mock 下面兩點:
- 網絡請求(用 MSW)
- 實現動畫的組件(因為誰會想在測試里等待呀)
下面的測試用例會渲染整個應用。但這不是集成測試的硬性要求,而且大多數我寫的集成測試都不會渲染整個 App。他們一般只會渲染 App 里要用到的 Provider(這就是 test/app-test-utils 偽模塊中 render 要做的事):
import * as React from 'react' import {render, screen, waitForElementToBeRemoved} from 'test/app-test-utils' import userEvent from '@testing-library/user-event' import {build, fake} from '@jackfranklin/test-data-bot' import {rest} from 'msw' import {setupServer} from 'msw/node' import {handlers} from 'test/server-handlers' import App from '../app'const buildLoginForm = build({fields: {username: fake(f => f.internet.userName()),password: fake(f => f.internet.password()),}, })// 集成測試一般只會用 MSW 這個庫來 Mock HTTP 請求 const server = setupServer(...handlers)beforeAll(() => server.listen()) afterAll(() => server.close()) afterEach(() => server.resetHandlers())test(`logging in displays the user's username`, async () => {// 這個自定義的 render 會在 App 加載完成時返回一個 Promise// (如果你用服務器渲染,你可能不需要這么做)// 這個自定義的 render 還可以讓你指定你的初始路由await render(<App />, {route: '/login'})const {username, password} = buildLoginForm()userEvent.type(screen.getByLabelText(/username/i), username)userEvent.type(screen.getByLabelText(/password/i), password)userEvent.click(screen.getByRole('button', {name: /submit/i}))await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i))// 檢查用戶是否已經登錄了expect(screen.getByText(username)).toBeInTheDocument() })對這樣的測試,一般我會做一些全局處理,比如 自動重置所有 Mock。
你可以在 React Testing Library setup docs 里了解更多關于上面的測試工具函數。
單元測試
import '@testing-library/jest-dom/extend-expect' import * as React from 'react' // 如果你的集成測試里有像上面一樣的測試工具模塊 // 那別用 @testing-library/react,直接用你的就好了 import {render, screen} from '@testing-library/react' import ItemList from '../item-list'// 有些人可能不會把這樣的測試叫做單測,因為我們還是要用 React 渲染成 DOM // 他們還可能會告訴你要用 shallow render // 當他們跟你說這個的時候,請把這個鏈接 https://kcd.im/shallow 甩他們臉上 test('renders "no items" when the item list is empty', () => {render(<ItemList items={[]} />)expect(screen.getByText(/no items/i)).toBeInTheDocument() })test('renders the items in a list', () => {render(<ItemList items={['apple', 'orange', 'pear']} />)// 注意:為了簡化這個例子,這里用了 snapshot,不過僅限于:// 1. snapshot 很小// 2. 我們用了 toMatchInlineSnaphost// 詳情:Read more: https://kcd.im/snapshotsexpect(screen.getByText(/apple/i)).toBeInTheDocument()expect(screen.getByText(/orange/i)).toBeInTheDocument()expect(screen.getByText(/pear/i)).toBeInTheDocument()expect(screen.queryByText(/no items/i)).not.toBeInTheDocument() })相信所有人都知道下面這個肯定是單測:
// 純函數是單元測試的最佳選擇,我還喜歡用 jest-in-case 來做單測 import cases from 'jest-in-case' import fizzbuzz from '../fizzbuzz'cases('fizzbuzz',({input, output}) => expect(fizzbuzz(input)).toBe(output),[[1, '1'],[2, '2'],[3, 'Fizz'],[5, 'Buzz'],[9, 'Fizz'],[15, 'FizzBuzz'],[16, '16'],].map(([input, output]) => ({title: `${input} => ${output}`, input, output})), )靜態測試
(譯注:靜態測試這里其實更多是指用 TypeScript 以及 ESLint 等靜態檢查工具來找出代碼問題)
// 你能發現下面的問題么? // 我相信 ESLint 的 for-direction 規則會比你 code review 時 // 更快發現這個問題 😉 for (var i = 0; i < 10; i--) {console.log(i) }const two = '2' // 這個有點挑剔了,不過 TypeScript 會告訴你這么做是不好的 const result = add(1, two)測試的初衷
記住我們為什么寫測試是很重要的。為什么你要寫測試?是因為我讓你寫?是因為如果不寫測試你的 PR 無法通過?還是因為測試可以提升開發體驗?
我寫測試最大、最重要的原因就是 CONFIDENCE(代碼信心)。我希望能夠信任未來我寫的代碼不會在上線時弄崩整個應用。所以,無論如何,
我都想確保這些類型的測試都能給我來帶來最大的 CONFIDENCE,所以在做測試時,要對它們做一些權衡。
如何取舍
我在這張圖里列了一些比較重要的點:
上面的箭頭代表了你在寫自動化測試時要關注的 3 個取舍點:
成本:¢ heap ? 💰🤑💰
當你往模型的上面走時,測試的成本會變得非常高。這不僅來自來真實在 CI 環境上跑所花的錢,還來自開發自己要編寫和維護單個獨立測試所花的時間。
越往模型上方走,遇到的報錯和失敗就越多,測試就越容易崩,從而導致需要更多時間來分析和修復測試。記住這句話,等會要考。
速度:🏎💨 ? 🐢
越往模型上方走,測試則跑得越慢。這是由于越跑高層級的測試,你就要跑更多的代碼。
而對單測來說,一般只測沒有依賴的小代碼片段,或者把依賴給 Mock 掉(會把上千行的代碼替換成簡單幾行)。記住這句話,等會要考。
信心:簡單問題 👌 ? 大問題 😖
一般人們在聊測試金字塔模型 🔺 時,都會聊到測試成本和速度的取舍。如果只考慮這兩點,那對于這個金字塔模型,我肯定 100% 把精力放單測上,而
不去管其它的測試類型。當然我不能這么做,這是因為還有一個超級重要的因素,可能你經常會聽我說到它:
當你的測試和你應用的使用方式越相似,它們能給你的信心就越大。
這是什么意思呢?意思是只有在用戶真實使用過后,才能保證你的應用是正常工作的。但我們不可能真的去等一個真實的用戶來找 Bug 吧?這會要很長時間,而且他可能會錯過一些我們可能應該測試的功能。再加上我們會定期發布軟件更新,任何人都無法用上最新的版本。
所以要怎么解決?我們要做權衡。 那我們應該怎么做?我們可以寫測試來測自己的應用,而當我們的測試不能像真實用戶那樣測試我們的應用時,我們就要對不同測試做權衡,只有這樣才能解決實際問題。這就是這個測試模型中每一層我們要做的事。
當你往測試模型的上方走時,你也同時在提升我所說的 “信心系數”。 這是你在那一層里能夠給你相對其它層的信心。你可以把模型最上層的測試想象成手動測試,這肯定會給你非常強的信心,相對地,它們成本也很高,速度也很慢。
還記得剛剛就讓你記住兩件事么:
- 越往上走,遇到的報錯和失敗就越多,因此你的測試也越容易崩
- 單測一般只用來測無依賴的小東西,或者把它的依賴 Mock 掉再測試(把上千行代碼替換成幾行 Mock 實現)
這兩點說的都是:越往下走,你能測到的代碼就越少。所以,如果你在做低層級的測試,會需要更多測試用例來覆蓋應用程序中相同數量的代碼。實際上,當你越往模型下面走,會有很多東西是沒辦法測試的。
說一下這些測試的問題,靜態分析工具無法給你帶來任何對業務邏輯的信心。單測也無法確保你是否正確地使用依賴的(雖然你可以用斷言判斷它們是怎么被調用的,但是你仍然無法確保它在單測里是否被正確調用了)。UI 集成測試則是無法確保你是否正確把參數傳給后端,以及是否正確處理返回錯誤。E2E 確實很好,但一般來說你只會把它們放在測試環境下跑(類生產環境,但是不是真生產環境)來獲取相對較高的代碼信心。
現在讓我們從另一個角度出發:在模型的頂端,如果你想用 E2E 來檢查輸入文本和點擊提交后表單的邊界用例,你需要啟動整個應用來做很多初始準備工作(后端也要),對這樣場景來說,用集成測試會更合適。而如果你想用集成測試來測試優惠券的邊界情況,你可能要在初始函數里做一些準備工作來渲染出優惠券生成組件,然后才能通過單測更好地覆蓋邊界用例。而如果你想用單測來驗證 add 函數沒有傳 number 而傳了 string 類型的情況,使用像 TypeScript 這樣的靜態類型檢查工具能更好地做驗證。
總結
模型里每個級別都有自己的優劣。一個 E2E 測試會失敗很多次,所以很難追蹤哪些代碼導致的崩潰,但這也意味著它能給你帶來更多的信心。這樣的測試在你沒有時間寫測試時是很有用的。我寧愿面對失敗多次的 E2E 測試,獲得更多代碼信心,也不想因為沒寫而要處理更多的 Bug。
最后,我其實不在乎這些測試類型之間的區別。 如果你說我的單測就是集成測試,或者甚至說是 E2E 測試(可能有人真的這么覺得 🤷?♂?),那就說吧。而我更關心的是:它們能否給我足夠的信心去改以前的代碼和實現新業務,所以我會結合不同的測試策略來達到這個目標。
好了,這篇外文就給大家帶到這里了。文章主要講了 4 種測試類型:靜態、單測、集成、E2E。其實在寫測試的過程當中,是很難區分你到底是在寫哪種測試的,也不用一直想著:我在寫哪類的測試、項目測試種類的比例怎么分、測試數量多少的問題。就像 Kent 說的那樣:我根本不在乎我寫的是啥,我只在乎它是否能提高我的代碼信心就足夠了。
如果你喜歡我的分享,可以來一波一鍵三連,點贊、在看就是我最大的動力,比心 ??
總結
以上是生活随笔為你收集整理的前端测试一共有哪几种?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [C语言练习题 7] 通过移位运算(<<
- 下一篇: 浏览器如何屏蔽csdn广告