對於像使用React 構建的交互式網站,集成測試是自然而然的選擇。它們驗證用戶與應用程序的交互方式,而無需端到端測試的額外開銷。
本文通過一個練習來闡述,該練習從一個簡單的網站開始,使用單元測試和集成測試驗證行為,並演示集成測試如何通過更少的代碼行實現更大的價值。本文內容假設您熟悉React 和JavaScript 中的測試。熟悉Jest 和React Testing Library 會有所幫助,但不是必需的。
測試分為三種類型:
- 單元測試獨立驗證一段代碼。它們易於編寫,但可能會忽略大局。
- 端到端測試(E2E) 使用自動化框架(例如Cypress 或Selenium)像用戶一樣與您的網站交互:加載頁面、填寫表單、點擊按鈕等。它們通常編寫和運行速度較慢,但與真實的用戶體驗非常接近。
- 集成測試介於兩者之間。它們驗證應用程序的多個單元如何協同工作,但比E2E 測試更輕量級。例如,Jest 自帶一些內置實用程序來促進集成測試;Jest 在後台使用jsdom 來模擬常見的瀏覽器API,其開銷小於自動化,並且其強大的模擬工具可以模擬外部API 調用。
另一個需要注意的地方:在React 應用程序中,單元測試和集成測試的編寫方式相同,使用的工具也相同。
開始進行React 測試
我創建了一個簡單的React 應用程序(可在GitHub 上找到),其中包含一個登錄表單。我將其連接到reqres.in,這是一個我發現用於測試前端項目的方便的API。
您可以成功登錄:
…或者遇到來自API 的錯誤消息:
代碼結構如下:
<code>LoginModule/ ├── components/ │ ├── Login.js // 渲染LoginForm、错误消息和登录确认│ └── LoginForm.js // 渲染登录表单字段和按钮├── hooks/ │ └── useLogin.js // 连接到API 并管理状态└── index.js // 将所有内容整合在一起</code>
選項1:單元測試
如果您像我一樣喜歡編寫測試——也許戴著耳機,在Spotify 上播放著不錯的音樂——那麼您可能會忍不住為每個文件編寫單元測試。
即使您不是測試愛好者,您也可能正在參與一個“試圖做好測試”的項目,但沒有明確的策略,測試方法是“我想每個文件都應該有自己的測試?”
這看起來像這樣(為了清晰起見,我在測試文件名中添加了unit):
<code>LoginModule/ ├── components/ │ ├── Login.js │ ├── Login.unit.test.js │ ├── LoginForm.js │ └── LoginForm.unit.test.js ├── hooks/ │ ├── useLogin.js │ └── useLogin.unit.test.js ├── index.js └── index.unit.test.js</code>
我在GitHub 上完成了添加所有這些單元測試的練習,並創建了一個test:coverage:unit 腳本以生成覆蓋率報告(Jest 的內置功能)。我們可以通過四個單元測試文件實現100% 的覆蓋率:
100% 的覆蓋率通常是過度的,但對於如此簡單的代碼庫來說是可以實現的。
讓我們深入研究為onLogin React hook 創建的單元測試之一。如果您不熟悉React hook 或如何測試它們,請不要擔心。
test('successful login flow', async () => { // 模擬成功的API 響應 jest .spyOn(window, 'fetch') .mockResolvedValue({ json: () => ({ token: '123' }) }); const { result, waitForNextUpdate } = renderHook(() => useLogin()); act(() => { result.current.onSubmit({ email: '[email protected]', password: 'password', }); }); // 將狀態設置為pending expect(result.current.state).toEqual({ status: 'pending', user: null, error: null, }); await waitForNextUpdate(); // 將狀態設置為resolved,存儲電子郵件地址 expect(result.current.state).toEqual({ status: 'resolved', user: { email: '[email protected]', }, error: null, }); });
這個測試寫起來很有趣(因為React Hooks Testing Library 使測試hook 變得輕而易舉),但它有一些問題。
首先,測試驗證內部狀態從'pending' 更改為'resolved';此實現細節不會向用戶公開,因此,可能不是要測試的好東西。如果我們重構應用程序,我們將不得不更新此測試,即使從用戶的角度來看沒有任何變化。
此外,作為單元測試,這只是其中一部分。如果我們想驗證登錄流程的其他功能,例如提交按鈕文本更改為“加載中”,我們將不得不在不同的測試文件中進行操作。
選項2:集成測試
讓我們考慮添加一個集成測試來驗證此流程的替代方法:
<code>LoginModule/ ├── components/ │ ├── Login.js │ └── LoginForm.js ├── hooks/ │ └── useLogin.js ├── index.js └── index.integration.test.js</code>
我實現了這個測試和一個test:coverage:integration 腳本以生成覆蓋率報告。就像單元測試一樣,我們可以達到100% 的覆蓋率,但這次都在一個文件中,並且需要的代碼行更少。
以下是涵蓋成功登錄流程的集成測試:
test('successful login', async () => { jest .spyOn(window, 'fetch') .mockResolvedValue({ json: () => ({ token: '123' }) }); render(<loginmodule></loginmodule> ); const emailField = screen.getByRole('textbox', { name: 'Email' }); const passwordField = screen.getByLabelText('Password'); const button = screen.getByRole('button'); // 填寫並提交表單fireEvent.change(emailField, { target: { value: '[email protected]' } }); fireEvent.change(passwordField, { target: { value: 'password' } }); fireEvent.click(button); // 它設置加載狀態expect(button).toBeDisabled(); expect(button).toHaveTextContent('Loading...'); await waitFor(() => { // 它隱藏表單元素expect(button).not.toBeInTheDocument(); expect(emailField).not.toBeInTheDocument(); expect(passwordField).not.toBeInTheDocument(); // 它顯示成功文本和電子郵件地址const loggedInText = screen.getByText('Logged in as'); expect(loggedInText).toBeInTheDocument(); const emailAddressText = screen.getByText('[email protected]'); expect(emailAddressText).toBeInTheDocument(); }); });
我真的很喜歡這個測試,因為它從用戶的角度驗證了整個登錄流程:表單、加載狀態和成功確認消息。集成測試非常適合React 應用程序,正是因為這種用例;用戶體驗是我們想要測試的內容,而這幾乎總是涉及多個不同的代碼片段協同工作。
此測試不了解使預期行為起作用的組件或hook,這很好。只要用戶體驗保持不變,我們就可以重寫和重構這些實現細節而不會破壞測試。
我不會深入研究登錄流程的初始狀態和錯誤處理的其他集成測試,但我鼓勵您在GitHub 上查看它們。
那麼,什麼需要單元測試?
與其考慮單元測試與集成測試,不如讓我們退一步,考慮一下我們如何決定首先需要測試什麼。需要測試LoginModule,因為它是一個我們希望使用者(應用程序中的其他文件)能夠放心地使用的實體。
另一方面,不需要測試onLogin hook,因為它只是LoginModule 的實現細節。但是,如果我們的需求發生變化,並且onLogin 在其他地方有用例,那麼我們將需要添加我們自己的(單元)測試來驗證其作為可重用實用程序的功能。 (我們也需要移動該文件,因為它不再特定於LoginModule 了。)
單元測試仍然有很多用例,例如需要驗證可重用選擇器、hook 和普通函數。在開發代碼時,您可能還會發現使用單元測試進行測試驅動開發很有幫助,即使您稍後將該邏輯向上移動到集成測試。
此外,單元測試在針對多個輸入和用例進行詳盡測試方面做得很好。例如,如果我的表單需要針對各種場景(例如無效電子郵件、缺少密碼、密碼過短)顯示內聯驗證,我將在集成測試中涵蓋一個代表性案例,然後在單元測試中深入研究具體案例。
其他好處
既然我們在這裡,我想談談一些幫助我的集成測試保持清晰和有序的語法技巧。
清晰的waitFor 塊
我們的測試需要考慮LoginModule 的加載狀態和成功狀態之間的延遲:
const button = screen.getByRole('button'); fireEvent.click(button); expect(button).not.toBeInTheDocument(); // 太快了,按鈕還在!
我們可以使用DOM Testing Library 的waitFor 輔助函數來做到這一點:
const button = screen.getByRole('button'); fireEvent.click(button); await waitFor(() => { expect(button).not.toBeInTheDocument(); // 啊,好多了});
但是,如果我們還想測試其他一些項目呢?網上沒有很多關於如何處理此問題的好的示例,並且在過去的項目中,我已經將其他項目放在waitFor 之外:
// 等待按鈕await waitFor(() => { expect(button).not.toBeInTheDocument(); }); // 然後測試確認消息const confirmationText = getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument();
這有效,但我不喜歡它,因為它使按鈕條件看起來很特殊,即使我們可以輕鬆地切換這些語句的順序:
// 等待確認消息await waitFor(() => { const confirmationText = getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument(); }); // 然後測試按鈕expect(button).not.toBeInTheDocument();
在我看來,將與相同更新相關的所有內容一起分組到waitFor 回調中要好得多:
await waitFor(() => { expect(button).not.toBeInTheDocument(); const confirmationText = screen.getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument(); });
對於像這樣的簡單斷言,我真的很喜歡這種技術,但在某些情況下,它可能會減慢測試速度,等待在waitFor 之外立即發生的失敗。有關此示例,請參閱React Testing Library 常用錯誤中的“在單個waitFor 回調中有多個斷言”。
對於包含幾個步驟的測試,我們可以連續使用多個waitFor 塊:
const button = screen.getByRole('button'); const emailField = screen.getByRole('textbox', { name: 'Email' }); // 填寫表單fireEvent.change(emailField, { target: { value: '[email protected]' } }); await waitFor(() => { // 檢查按鈕是否已啟用 expect(button).not.toBeDisabled(); expect(button).toHaveTextContent('Submit'); }); // 提交表單fireEvent.click(button); await waitFor(() => { // 檢查按鈕是否不再存在 expect(button).not.toBeInTheDocument(); });
如果您只等待一個項目出現,則可以使用findBy 查詢代替。它在後台使用waitFor。
行內it 註釋
另一個測試最佳實踐是編寫更少、更長的測試;這使您可以將測試用例與重要的用戶流程關聯起來,同時使測試保持隔離,以避免意外行為。我贊成這種方法,但它在保持代碼組織和記錄所需行為方面可能會帶來挑戰。我們需要未來的開發人員能夠返回測試並了解它在做什麼,為什麼它會失敗等等。
例如,假設這些期望之一開始失敗:
it('handles a successful login flow', async () => { // 為清晰起見隱藏測試的開頭 expect(button).toBeDisabled(); expect(button).toHaveTextContent('Loading...'); await waitFor(() => { expect(button).not.toBeInTheDocument(); expect(emailField).not.toBeInTheDocument(); expect(passwordField).not.toBeInTheDocument(); const confirmationText = screen.getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument(); }); });
查看此內容的開發人員無法輕鬆確定正在測試的內容,並且可能難以確定失敗是錯誤(這意味著我們應該修復代碼)還是行為更改(這意味著我們應該修復測試)。
我最喜歡的解決方案是使用每個測試的鮮為人知的測試語法,並添加描述正在測試的每個關鍵行為的行內it 樣式註釋:
test('successful login', async () => { // 為清晰起見隱藏測試的開頭 // 它設置加載狀態 expect(button).toBeDisabled(); expect(button).toHaveTextContent('Loading...'); await waitFor(() => { // 它隱藏表單元素 expect(button).not.toBeInTheDocument(); expect(emailField).not.toBeInTheDocument(); expect(passwordField).not.toBeInTheDocument(); // 它顯示成功文本和電子郵件地址 const confirmationText = screen.getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument(); }); });
這些註釋不會神奇地與Jest 集成,因此如果您遇到失敗,失敗的測試名稱將對應於您傳遞給測試標籤的參數,在本例中為“successful login”。但是,Jest 的錯誤消息包含周圍的代碼,因此這些it 註釋仍然有助於識別失敗的行為。當我從一個期望中刪除not 時,我收到了以下錯誤消息:
為了獲得更明確的錯誤,有一個名為jest-expect-message 的包允許您為每個期望定義錯誤消息:
expect(button, 'button is still in document').not.toBeInTheDocument();
一些開發人員更喜歡這種方法,但我發現它在大多數情況下有點太granular 了,因為單個it 通常涉及多個期望。
團隊的後續步驟
有時我希望我們可以為人類製定linter 規則。如果是這樣,我們可以為我們的團隊設置一個prefer-integration-tests 規則,然後就結束了。
但是,唉,我們需要找到一個更類似的解決方案來鼓勵開發人員在某些情況下選擇集成測試,例如我們前面介紹的LoginModule 示例。像大多數事情一樣,這歸結於團隊討論您的測試策略,就對項目有意義的內容達成一致,並且——希望——在ADR 中記錄它。
在製定測試計劃時,我們應該避免一種會迫使開發人員為每個文件編寫測試的文化。開發人員需要能夠放心地做出明智的測試決策,而不必擔心他們“測試不足”。 Jest 的覆蓋率報告可以通過提供一個健全性檢查來幫助解決這個問題,即使測試在集成級別上進行了合併。
我仍然不認為自己是集成測試專家,但是進行這項練習幫助我分解了一個集成測試比單元測試提供更大價值的用例。我希望與您的團隊分享這一點,或者在您的代碼庫上進行類似的練習,將有助於指導您將集成測試納入您的工作流程。
以上是反應集成測試:覆蓋範圍更大,測試較少的詳細內容。更多資訊請關注PHP中文網其他相關文章!

重構自己的代碼看起來是什麼樣的?約翰·瑞亞(John Rhea)挑選了他寫的一個舊的CSS動畫,並介紹了優化它的思維過程。

CSSanimationsarenotinherentlyhardbutrequirepracticeandunderstandingofCSSpropertiesandtimingfunctions.1)Startwithsimpleanimationslikescalingabuttononhoverusingkeyframes.2)Useeasingfunctionslikecubic-bezierfornaturaleffects,suchasabounceanimation.3)For

@keyframesispopularduetoitsversatoryand and powerincreatingsmoothcsssanimations.keytricksinclude:1)definingsmoothtransitionsbetnestates,2)使用AnimatingMultatingMultationMultationProperPertiessimultane,3)使用使用4)使用BombingeNtibalibility,4)使用CombanningWiThjavoFofofofoftofofo

CSSCOUNTERSAREDOMANAGEAUTOMANAMBERINGINWEBDESIGNS.1)他們可以使用forterablesofcontents,ListItems,and customnumbering.2)AdvancedsincludenestednumberingSystems.3)挑戰挑戰InclassINCludeBrowsEccerCerceribaliblesibility andperformiballibility andperformissises.4)創造性

使用滾動陰影,尤其是對於移動設備,是克里斯以前涵蓋的一個微妙的UX。傑夫(Geoff)涵蓋了一種使用動畫限制屬性的新方法。這是另一種方式。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能

SublimeText3 Linux新版
SublimeText3 Linux最新版

mPDF
mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

Safe Exam Browser
Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

Dreamweaver Mac版
視覺化網頁開發工具