# 前端單元測試 筆記 不能測試,所以要重構(把 code 變乾淨) 連線成本高,所以要 mock。每個 mock 手法又是不同的難度。但前後要花好多時間才能走到關鍵。 ![](https://i.imgur.com/mJwcKDE.png) ![](https://i.imgur.com/un26znX.png) 為什麼要測試? 抓到上線會出現的問題,我有信心不加班。sideEffect 有沒有被改壞。 ![](https://i.imgur.com/W85sPeu.png) 根據現有的時間,決定要怎麼測試。不用糾結到底要寫多純。 ![](https://i.imgur.com/qVWXy5l.png) # Jest [jest-watch-typeahead](https://www.npmjs.com/package/jest-watch-typeahead) test.only 只測試某項 test.skip 跳過某項 # React Testing Library react-testing library 有提供 [testing-playground](https://testing-playground.com/),告訴你該怎麼取到 element 取的方式不同,assertion 也會不同。 測試完成要記得回到原本的扣改壞看看。 # snap shot toMatchSnapshot - 會產生一個資料夾 toMatchInlineSnapshot - 在原本的測試檔案裡面 # userEvent vs fireEvent userEvent 是站在使用者的角度出發,不管你底層技術是如何實作。例如按按鈕,onMouseDown or onClick 都會達到相同的效果。好處是背後技術換了,測試不用一直改,因為從使用者的角度來看,都是一樣的。 fireEvent 要精準地告訴他,是哪一種事件 # mock 使用 jest timer 不用在原地等三十秒 jest.useFakeTimer() jest.runAllTimer() jest.advanceTimerByTime() # act react test utilities 等 component 的 lifecycle 重新 render 完成,避免一些 side effect ![](https://i.imgur.com/eZYZxjO.png) fireEvent 為什麼不用包 act ? 因為 fireEvent and userEvent 已經內建包好了 # styled component styled component 會產生測試的亂碼,如果 css 有改變,測試就死掉了。但無法透過肉眼辨識快照的差異。 jest-styled-components 透過這個軟件 # screen vs container screen 整張 html render 的東西 container 是針對 render(<這個東東/>) render 的東西 ```js screen.debug() debug(container) debug(container.firstChild) ``` # subtest & test each (不一樣 data 重複的事情) 每個情境都寫一樣,很不必要。因為只是參數和結果不一樣。 ```js import { screen, render } from "@testing-library/react"; import Button from "../../exercise/06"; import { ButtonType } from "../../exercise/06types"; // before describe("Button with diferrent type", () => { test("with type A", async () => { render(<Button type={ButtonType.A} />); expect(screen.getByText(/hi/i)).toHaveClass("eb-bg-green-500"); }); test("with type B", async () => { render(<Button type={ButtonType.B} />); expect(screen.getByText(/hi/i)).toHaveClass("eb-bg-blue-500"); }); test("with type C", async () => { render(<Button type={ButtonType.C} />); expect(screen.getByText(/hi/i)).toHaveClass("eb-bg-red-500"); }); test("with type X", async () => { render(<Button type={"X"} />); expect(screen.getByText(/hi/i)).toHaveClass("eb-bg-green-500"); }); }); ``` 故意把某個用壞,看不到之後的狀況 ```js import { screen, render } from "@testing-library/react"; import Button from "../../exercise/06"; import { ButtonType } from "../../exercise/06types"; // before describe("Button with diferrent type", () => { test('Button with all types', async() => { const {rerender} = render(<Button type={ButtonType.A} />); expect(screen.getByText(/hi/i)).toHaveClass("eb-bg-green-500"); rerender(<Button type={ButtonType.B} />); expect(screen.getByText(/hi/i)).toHaveClass("eb-bg-bluess-500"); rerender(<Button type={ButtonType.C} />); expect(screen.getByText(/hi/i)).toHaveClass("eb-bg-redss-500"); rerender(<Button type={"X"} />); expect(screen.getByText(/hi/i)).toHaveClass("eb-bg-green-500"); }) }); ``` ```js test.each([[ButtonType.A, "eb-bg-green-500"], [ButtonType.B, "eb-bg-blue-500"], [ButtonType.C, "eb-bg-red-500"], ["X", "eb-bg-green-500"], ], ) ('type %s will have className %s', (type, className) => { render(<Button type={type}/>) expect(screen.getByText(/hi/i)).toHaveClass(className) }) test.each([ {type: ButtonType.A, className: "eb-bg-green-500"}, {type: ButtonType.B, className: "eb-bg-blue-500"}, {type: ButtonType.C, className: "eb-bg-red-500"}, {type: "X", className: "eb-bg-green-500"} ])("type $type will have className $className", ({type, className}) => { render(<Button type={type}/>) expect(screen.getByText(/hi/i)).toHaveClass(className) }) test.each` type | className ${ButtonType.A} | ${"eb-bg-green-500"} ${ButtonType.B} | ${"eb-bg-blue-500"} ${ButtonType.C} | ${"eb-bg-red-500"} ${"X"} | ${"eb-bg-green-500"} `("type $type will have className $className", ({type, className}) => { render(<Button type={type}/>) expect(screen.getByText(/hi/i)).toHaveClass(className) }) ``` describe 的層級也可以用 each 來做 # msw reset handlers 是因為在測試的過程中可能改變初始的定義,所以要將他還原回原本的樣子 waitFor 等待 react component side effect done findby = getBy + waitFor # Hook hook 不能單獨存在,每次寫都要把它包進來 ```js import { render, act } from "@testing-library/react"; import useCounter from "../../exercise/08"; test("should increment counter", () => { // gogo let result function TestCounter () { result = useCounter(); return null } render(<TestCounter/>) expect(result.count ).toBe(0) act(() => { result.increment() }) expect(result.count).toBe(1) }); ``` 透過 [react-hooks-testing-library](https://react-hooks-testing-library.com/),來簡化測試 hook 碰到煩人步驟 ## 常用 ```js // import userEvent from '@testing-library/user-event' expect(screen.getByRole('button')).toHaveTextContent(content); jest.mock("../../exercise/09/Trade", () => () => "FakeTradeComponent"); // mock component expect(screen.getByTestId("sell").textContent).toBe("賣完,得到 $10400"); ``` @testing-library/react - within usage # EX Trade 裡面有亂數,這邊要額外處理 結構變得單純,可以考慮用快照直接照起來 # 參考資料 * winsome 在學 youtube # 尾聲 * ![](https://i.imgur.com/nVTxPPe.png) * django