<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端掌握單元測試-jest

          共 17691字,需瀏覽 36分鐘

           ·

          2022-07-07 18:22

          本文適合對單元測試感興趣的小伙伴閱讀

          歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進階~

          一、前言

          本文基于開源項目:

          https://github.com/facebook/jest

          https://www.jestjs.cn/

              對于單元測試,可能小伙伴們的第一反應都是“難”,能不寫一般就不去寫了。廣東靚仔也覺得寫單元測試是個有挑戰(zhàn)性,且有難度的任務,但廣東靚仔覺得大家可以盡量去嘗試寫一寫單元測試,在bug減少的同時,項目的質(zhì)量也有很大的提升,對個人而言一定能提升我們自己的能力。
              本文我們一起來看看Jest,Jest現(xiàn)在已經(jīng)更新到了28~

          二、what Jest

          Jest 是一個令人愉快的 JavaScript 測試框架,專注于"簡潔明快"。
          這些項目都在使用 Jest:Babel、 TypeScript、 Node、 React、 Angular、 Vue 等等!

          特點:

          ??????? 零配置:Jest 的目標是在大部分 JavaScript 項目上實現(xiàn)開箱即用, 無需配置。
          ??快照測試:能夠輕松追蹤大型對象的測試。快照可以與測試代碼放在一起,也可以集成進代碼行內(nèi)。
          ???? 隔離:測試程序擁有自己獨立的進程 以最大限度地提高性能。
          ??????? 優(yōu)秀的api:從 it 到 expect - Jest 將整個工具包放在同一個 地方。好書寫、好維護、非常方便。

          三、入門

          安裝 Jest:npm / yarn

          npm install --save-dev jest
          # or
          yarn add --dev jes

          一般在選中哪個版本的時候,廣東靚仔建議使用穩(wěn)定的版本即可,不一定要最新。

          (@27版本)初始化【@28可以省略這一步】

          npx jest --init

          執(zhí)行完后能看到如下文件(翻譯了一下):

          export default {
            // 測試中所有導入的模塊都應該自動模擬
            // automock: false,
            // `n` 次失敗后停止運行測試
            // bail: 0,
            // Jest 應該存儲其緩存的依賴信息的目錄
            // 每次測試前自動清除模擬調(diào)用、實例、上下文和結(jié)果
            // 開啟覆蓋率
            clearMockstrue,
            // 指示是否應在執(zhí)行測試時收集覆蓋率信息
            collectCoveragetrue,
            // 一組 glob 模式,指示應為其收集覆蓋信息的一組文件
            // collectCoverageFrom: undefined,
            // Jest 應該輸出其覆蓋文件的目錄
            coverageDirectory"coverage",
            // 用于跳過覆蓋收集的正則表達式模式字符串數(shù)組
            // coveragePathIgnorePatterns: [
            //   "\\\\node_modules\\\\"
            // ],

            // 指示應使用哪個提供程序來檢測代碼以進行覆蓋
            coverageProvider"v8",
          };

          Demo

          Tips: 一般單元測試建議寫在utils文件夾下。

          目錄如下:

          ├── jest.config.js
          ├── package-lock.json
          ├── package.json
          ├── src
          │   └── utils
          │       └── sum.js
          └── liangzai-tests
              └── utils
                  └── sum.test.js

            /utils/sum.js

          // sum.js
          function sum(a, b{
            return a + b;
          }
          module.exports = sum;

          /liangzai-utils/sum.test.js

          const sum = require('../../utils/sum');

          test('adds 1 + 2 to equal 3', () => {
            expect(sum(12)).toBe(3);
          });

          然后執(zhí)行

          npm test

          可以看到結(jié)果:

          四、轉(zhuǎn)譯ts

          Jest 本身不做代碼轉(zhuǎn)譯工作:

          安裝ts

          npm i -D typescript@4.6.3

          初始化 TypeScript 的配置

          npx tsc --init

          執(zhí)行后會看到tsconfig.json 文件:

          {
            "compilerOptions": {
              "types": ["node""jest"],
              "target""es2016",                                  
              /* 為發(fā)出的 JavaScript 設(shè)置 JavaScript 語言版本并包含兼容的庫聲明 */
              "module""commonjs",                                
              /* 指定生成什么模塊代碼. */
             "esModuleInterop"true,                             
             /* 發(fā)出額外的 JavaScript 以簡化對導入 CommonJS 模塊的支持。這將啟用 `allowSyntheticDefaultImports` 以實現(xiàn)類型兼容性. */
              "forceConsistentCasingInFileNames"true,            
              /* 確保imports中的大小寫正確 . */
              "strict"true,                                      
              /* 啟用所有嚴格的類型檢查選項。 */
              "skipLibCheck"true                                 
              /* 跳過類型檢查所有 .d.ts 文件. */
            }
          }

          修改.js為.ts,代碼增加類型

          const sum = (a: number, b: number) => {
            return a + b;
          }

          export default sum;

          安裝Jest 類型聲明包

          npm i -D @types/[email protected]

          最后執(zhí)行 npm run test,測試通過。

          小優(yōu)化

          路徑使用簡寫,修改 tsconfig.json 配置:

          {
            "compilerOptions": {
              "paths": {
                "@/*": ["src/*"]
              }
            }
          }

          jest.config.js修改moduleNameMapper

          modulex.exports = {
            "moduleNameMapper": {
              "@/(.*)""<rootDir>/src/$1"
            }
          }

          五、其他知識點

          setupFilesAfterEnv 和 setupFiles

          簡單來說:

          • setupFiles 是在 引入測試環(huán)境(比如下面的 jsdom)之后 執(zhí)行的代碼
          • setupFilesAfterEnv 可以指定一個文件,在每執(zhí)行一個測試文件前都會跑一遍里面的代碼
          具體應用場景是:在 setupFiles 可以添加 測試環(huán)境 的補充,比如 Mock 全局變量 abcd 等。而在 setupFilesAfterEnv 可以引入和配置 Jest/Jasmine(Jest 內(nèi)部使用了 Jasmine) 插件。

          jsdom 測試環(huán)境

          jest 提供了 testEnvironment 配置:

          module.exports = {
            testEnvironment"jsdom",
          }

          jsdom: 這個庫用 JS 實現(xiàn)了一套 Node.js 環(huán)境下的 Web 標準 API。

          添加 jsdom 測試環(huán)境后,全局會自動擁有完整的瀏覽器標準 API,不需要Mock了。

          引入react/vue

          step1: 安裝Webpack 依賴

          step2: 安裝相應的Loader

          step3: 安裝React/vue 以及業(yè)務

          這里列舉下webpack.config.js

          const path = require('path');
          const HtmlWebpackPlugin = require('html-webpack-plugin');

          module.exports = {
            mode'development',
            entry: {
              index'./src/index.tsx'
            },
            module: {
              rules: [
                // 解析 TypeScript
                {
                  test/\.(tsx?|jsx?)$/,
                  use'ts-loader',
                  exclude/(node_modules|tests)/
                },
                // 解析 CSS
                {
                  test/\.css$/i,
                  use: [
                    { loader'style-loader' },
                    { loader'css-loader' },
                  ]
                },
                // 解析 Less
                {
                  test/\.less$/i,
                  use: [
                    { loader"style-loader" },
                    {
                      loader"css-loader",
                      options: {
                        modules: {
                          mode(resourcePath) => {
                            if (/pure.css$/i.test(resourcePath)) {
                              return "pure";
                            }
                            if (/global.css$/i.test(resourcePath)) {
                              return "global";
                            }
                            return "local";
                          },
                        }
                      }
                    },
                    { loader"less-loader" },
                  ],
                },
              ],
            },
            resolve: {
              extensions: ['.tsx''.ts''.js''.less''css'],
              // 設(shè)置別名
              alias: {
                utils: path.join(__dirname, 'src/utils/'),
                components: path.join(__dirname, 'src/components/'),
                apis: path.join(__dirname, 'src/apis/'),
                hooks: path.join(__dirname, 'src/hooks/'),
                store: path.join(__dirname, 'src/store/'),
              }
            },
            devtool'inline-source-map',
            // 3000 端口打開網(wǎng)頁
            devServer: {
              static'./dist',
              port3000,
              hottrue,
            },
            // 默認輸出
            output: {
              filename'index.js',
              path: path.resolve(__dirname, 'dist'),
              cleantrue,
            },
            // 指定模板 html
            plugins: [
              new HtmlWebpackPlugin({
                template'./public/index.html',
              }),
            ],
          };

          package.json 添加啟動命令

          {
            "scripts": {
              "start""webpack serve",
              "test""jest"
            }
          }

          配置 tsconfig.json

          {
            "compilerOptions": {
              "jsx""react",
              "esModuleInterop"true,
              "baseUrl""./",
              "paths": {
                "utils/*": ["src/utils/*"],
                "components/*": ["src/components/*"],
                "apis/*": ["src/apis/*"],
                "hooks/*": ["src/hooks/*"],
                "store/*": ["src/store/*"]
              } 
            }
          }

          六、組件測試

          Demo: 這里列舉了一個簡單的場景

          user.ts: 獲取用戶角色身份

          import axios from "axios";

          // 類型:用戶角色身份
          export type UserRoleType = "user" | "admin";

          // 接口:返回
          export interface GetRoleRes {
            userType: UserRoleType;
          }

          // 函數(shù):獲取用戶角色身份
          export const getUserRole = async () => {
            return axios.get<GetRoleRes>("https://xxx.xx.com/api/role");
          };

          業(yè)務組件/Auth/Button/index.tsx(縮略代碼)

          import React, { FC, useEffect, useState } from "react";
          ...

          // 身份文案 Mapper
          const mapper: Record<UserRoleType, string> = {
            user"用戶",
            admin"管理員",
          };

          const Button: FC<Props> = (props) => {
            const { children, className, ...restProps } = props;

            const [userType, setUserType] = useState<UserRoleType>();

            // 獲取用戶身份,并設(shè)值
            const getLoginState = async () => {
              const res = await getUserRole();
              setUserType(res.data.userType);
            };

            useEffect(() => {
              getLoginState().catch((e) => message.error(e.message));
            }, []);

            return (
              <Button {...restProps}>
                {mapper[userType!] || ""}
                {children}
              </Button>

            );
          };

          export default Button;

          測試用例button.test.tsx

          import { render, screen } from "@testing-library/react";
          import Button from "components/Button";
          import React from "react";

          describe('Button', () => {
            it('可以正常展示', () => {
              render(<Button>登錄</Button>)

              expect(screen.getByText('登錄')).toBeDefined();
            });
          })

          上面這代碼只是一個簡單的Demo測試

          測試組件功能

          mockAxios.test.tsx

          import React from "react";
          import axios from "axios";
          import { render, screen } from "@testing-library/react";
          import Button from "components/Button";

          describe("Button Mock Axios", () => {
            it("可以正確展示用戶按鈕內(nèi)容"async () => {
              jest.spyOn(axios, "get").mockResolvedValueOnce({
                // 其它的實現(xiàn)...
                data: { userType"user" },
              });

              render(<Button>你好</Button>);

              expect(await screen.findByText("用戶你好")).toBeInTheDocument();
            });

            it("可以正確展示管理員按鈕內(nèi)容"async () => {
              jest.spyOn(axios, "get").mockResolvedValueOnce({
                // 其它的實現(xiàn)...
                data: { userType"admin" },
              });

              render(<Button>你好</Button>);

              expect(await screen.findByText("管理員你好")).toBeInTheDocument();
            });
          });

          當然,我們也可以不mock,而是使用 Http Mock 工具:msw

          Mock Http

          代碼如下:

          /mockServer/handlers.ts

          import { rest } from "msw";

          const handlers = [
            rest.get("https://xxx.xx.com/api/role"async (req, res, ctx) => {
              return res(
                ctx.status(200),
                ctx.json({
                  userType"user",
                })
              );
            }),
          ];

          export default handlers;

          /mockServer/server.ts

          import { setupServer } from "msw/node";
          import handlers from "./handlers";

          const server = setupServer(...handlers);

          export default server;

          /jest-setup.ts

          import server from "./mockServer/server";

          beforeAll(() => {
            server.listen();
          });

          afterEach(() => {
            server.resetHandlers();
          });

          afterAll(() => {
            server.close();
          });

          最后測試用例代碼:

          // 偏向真實用例
          import server from "../../mockServer/server";
          import { rest } from "msw";
          import { render, screen } from "@testing-library/react";
          import Button from "components/Button";
          import React from "react";
          import { UserRoleType } from "apis/user";

          // 初始化函數(shù)
          const setup = (userType: UserRoleType) => {
            server.use(
              rest.get("https://xxx.xx.com/api/role"async (req, res, ctx) => {
                return res(ctx.status(200), ctx.json({ userType }));
              })
            );
          };

          describe("Button Mock Http 請求", () => {
            it("可以正確展示普通用戶按鈕內(nèi)容"async () => {
              setup("user");

              render(<Button>廣東</Button>);

              expect(await screen.findByText("用戶你好")).toBeInTheDocument();
            });

            it("可以正確展示管理員按鈕內(nèi)容"async () => {
              setup("admin");

              render(<Button>靚仔</Button>);

              expect(await screen.findByText("管理員你好")).toBeInTheDocument();
            });
          });

          setup 函數(shù),在每個用例前初始化 Http 請求的 Mock 返回。

          七、小結(jié)

          Jest的功能遠不止于此,還能做性能測試自動化測試等等

          在我們閱讀完官方文檔后,我們一定會進行更深層次的學習,比如看下框架底層是如何運行的,以及源碼的閱讀。

              這里廣東靚仔給下一些小建議:
          • 在看源碼前,我們先去官方文檔復習下框架設(shè)計理念、源碼分層設(shè)計
          • 閱讀下框架官方開發(fā)人員寫的相關(guān)文章
          • 借助框架的調(diào)用棧來進行源碼的閱讀,通過這個執(zhí)行流程,我們就完整的對源碼進行了一個初步的了解
          • 接下來再對源碼執(zhí)行過程中涉及的所有函數(shù)邏輯梳理一遍

          關(guān)注我,一起攜手進階

          歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進階~

          瀏覽 124
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大香蕉人妻在线 | 大鸡巴视频在线观看 | A视频免费观看 | 欧美日韩在线视频免费播放 | 无码破解一区二区三区在线播报 |