###### tags: frontend, UT, 今晚,就該來點前端測試(P-4) # Note : Testing React with Jest and React Testing Library (RTL) (P-4) 終於,進入測試的主菜了:mock 老實說,我覺得 mock 真的有點複雜,複雜的點有兩個,第一:mock 的套件真的很多,真的是初心者的火球術~ 第二:我要測的內容中,那些東西應該是要 mock 起來哪些不用,然後我該怎麼順利的 mock QAQ 現在就讓我們從 server 的 data 開始 mock 吧~ 這邊我們選擇的是 msw,工作上用的是 axios-mock-adapter,但我個人偏好 msw,理由是 it works for axios, fetch, or any other http request 學習重點: 1. mock data from server 2. AAA pattern for testing (有助於你理解測試的架構,請自行 google) 3. 每次做非同步的操作時,請使用 await and findBy 4. server error response 5. debug tools # ex ![目標作業](https://i.imgur.com/h8PP0P6.jpg) 目標: * test the option images render * mock service worker: /scoops and /toppings # mock service worker why: * want to intercept network calls, and return specific responses. (functional test 基本上不應該涉及到 server) * prevents network calls during tests * set up tests conditons using server response how: 1. create handlers - handlers are functions that will determine what's returned for any particular urls or routes. 2. create test servers - make sure test server listens during all tests,會在 setupTest 檔案裡面做設定,也會在裡面 reset after each test [參照官方文件進行設定](https://mswjs.io/docs/getting-started/mocks/rest-api),src -> mocks/handlers.js ```js // src/mocks/handlers.js import { rest } from "msw"; export const handlers = [ rest.get("http://localhost:3030/scoops", (req, res, ctx) => { return res( ctx.status(200), ctx.json([ { name: "Mint chip", imagePath: "/images/mint-chip.png", }, { name: "Vanilla", imagePath: "/images/vanilla.png", }, ]) ); }), ]; ``` 和 express 有八七趴神似 :::success [HandlerType].HTTP_Method(url, (req, res, ctx) => {}) response resolver function 由三個部分組成 * req: req object * res: func to create response * ctx: utility to build response ::: 用 node 進行整合,將 create-react-app 進行設定,讓 msw 可以 intercept the network request,然後 return handlers 設定的內容作為 response ```js // setupTest.js // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import "@testing-library/jest-dom"; // src/setupTests.js import { server } from "./mocks/server.js"; // Establish API mocking before all tests. beforeAll(() => server.listen()); // Reset any request handlers that we may add during the tests, // so they don't affect other tests. afterEach(() => server.resetHandlers()); // Clean up after the tests are finished. afterAll(() => server.close()); ``` ## msw work in test Options Component -> make req to server ==(但發不出去,因為 msw 攔截請求,且回傳 handlers response)== -> Options Component ```js // options.test.js import { screen, render } from "@testing-library/react"; import Options from "../Options"; test("displays images for each scoop option from server", () => { render(<Options optionType="scoops" />); // find images const scoopImgs = screen.getAllByRole("img", { name: /scoop$/i }); expect(scoopImgs).toHaveLength(2); // confirm alt text of images const altText = scoopImgs.map((element) => element.alt); expect(altText).toEqual(["Mint chip scoop", "Vanilla scoop"]); // toEqual ex: arr or object }); ``` 測試寫好後,就來做畫面與功能,將 Options.js 內容補上 npm install axios ```js // Options.js import React, { useState, useEffect } from "react"; import axios from "axios"; import ScoopOptions from "./ScoopOptions"; export default function Options({ optionType }) { const [items, setItems] = useState([]); // TODO: replace null to ToppingOptions when available const ItemComponent = optionType === "scoops" ? ScoopOptions : null; const OptionsItems = items.map((item) => ( <ItemComponent key={item.name} name={item.name} imagePath={item.imagePath} /> )); useEffect(() => { // optionType is scoops or toppings (better using Enum) axios .get(`http://localhost:3030/${optionType}`) .then((response) => setItems(response.data)) .catch((err) => { // TODO: handle error response console.log(err); }); }, [optionType]); return <div>{OptionsItems}</div>; } ``` ```js // scoopOptions.js import { useState } from "react"; import { Col } from "antd"; export default function ScoopOptions({ name, imagePath }) { return ( <Col xs={12} sm={6} md={4} lg={3} style={{ textAlign: "center" }}> <img style={{ width: "75%" }} src={`http://localhost:3030/${imagePath}`} alt={`${name} scoop`} /> </Col> ); } ``` npm run test -> 測試死了:Warning: An update to Options inside a test was not wrapped in act(...) 和 TestingLibraryElementError: Unable to find an accessible element with the role "img" and name `/scoop$/i` 原因是,測試檔案在元件更新前就執行了 > 每次做非同步的操作時,請使用 await and findBy 修正如以下: ```js test("displays images for each scoop option from server", async () => { render(<Options optionType="scoops" />); // find images const scoopImgs = await screen.findAllByRole("img", { name: /scoop$/i }); expect(scoopImgs).toHaveLength(2); // confirm alt text of images const altText = scoopImgs.map((element) => element.alt); expect(altText).toEqual(["Mint chip scoop", "Vanilla scoop"]); // toEqual ex: arr or object }); ``` # Quize - topping image * add handlers for topping routes ```js // options.test.js test("displays images for each topping option from server", async () => { render(<Options optionType="toppings" />); const toppingImgs = await screen.findAllByRole("img", { name: /topping$/i }); expect(toppingImgs).toHaveLength(3); const altText = toppingImgs.map((element) => element.alt); expect(altText).toEqual([ "M&Ms topping", "Hot fudge topping", "Peanut butter cups topping", ]); }); ``` ```js // handlers.js rest.get("http://localhost:3030/toppings", (req, res, ctx) => { return res( ctx.json([ { name: "M&Ms", imagePath: "/images/m-and-ms.png", }, { name: "Hot fudge", imagePath: "/images/hot-fudge.png", }, { name: "Peanut butter cups", imagePath: "/images/peanut-butter-cups.png", }, ]) ); }), ``` ```js // options.js 大致調整 const ItemComponent = optionType === "scoops" ? ScoopOptions : ToppingOptions; ``` # server error response ![](https://i.imgur.com/PSjkVqz.png) * fill in catch statement if axios throw error * By default, handlers return non error response * override with error response for particular tests Recall: setup 過程 setupTest.js -> import server & listen server before all tests / reset handlers after each test / clean up after each are finished 更細部的看 server 會發現透過指定的 handler 去建立 server 的! --- ```js // OrderEntry.test.js import { findAllByRole, render, screen } from "@testing-library/react"; import OrderEntry from "../OrderEntry"; import { rest } from "msw"; // create new hadnlers import { server } from "../../../mocks/server"; // override the handlers test("handles error for scoops and toppings routes", async () => { server.resetHandlers( rest.get("http://localhost:3030/scoops", (req, res, ctx) => { return res(ctx.status(500)); }), rest.get("http://localhost:3030/toppings", (req, res, ctx) => { return res(ctx.status(500)); }) ); render(<OrderEntry />); const alerts = await screen.findAllByRole("alert", { name: /An unexpected error occurred. Please try again later./i, }); expect(alerts).toHaveLength(2); }); ``` 切版做功能 ```js // AlertBanner.js import { Alert } from "antd"; export default function AlertBanner({ message, type }) { const alertMessage = message || "An unexpected error occurred. Please try again later."; const alertType = type || "warning"; return <Alert type={alertType} message={alertMessage} />; } ``` ```js // Options.js const [error, setError] = useState(false); useEffect(() => { // optionType is scoops or toppings (better using Enum) axios .get(`http://localhost:3030/${optionType}`) .then((response) => setItems(response.data)) .catch((err) => { setError(true); }); }, [optionType]); return ( <div> {error && <AlertBanner />} {OptionsItems} </div> ); ``` ```js // orderEntry.js import Options from "./Options"; export default function OrderEntry() { return ( <> <Options optionType="scoops" /> <Options optionType="toppings" /> </> ); } ``` 然後,測試又死掉了 死掉的原因是 antd 在實作時,並沒有 name value,antd 的 alert 是透過 html div 去做的. The alert is an HTMLDivElement with an array of children that contains one Text element. By putting the Text element child into an array, it's not accessible as a name for the findAllByRole options. ![](https://i.imgur.com/onnL1IK.png) ps: if you we need to wait both of server call back, please use waitFor ```js const alerts = await screen.findAllByText( /An unexpected error occurred. Please try again later./i ); expect(alerts).toHaveLength(2); ``` <!-- 如果切版是使用 react-bootstrap 因為他實作的方式差異,findBy 會死掉 他沒有等到兩個非同步的請求都回來,解決方法 waitFor findBy is going to succeed when the first server call returned. But we need to wait both of them. ```js await waitFor(async () => { const alerts = await screen.findAllByText( /An unexpected error occurred. Please try again later./i ); expect(alerts).toHaveLength(2); }); ``` --> # Jest debug tools * only running one test file -> watch usage p * only running one test within a file nice way to isolate test: 1. test.only 2. test.skip ## 題外話 不會去測試 context 的實作,因為 functional testing 的精神在模擬使用者的使用情境,同理其他的 global state management 也是一樣的 only difference is the test setup * make sure component is wrappered in context * ensure functionality * avoids error