1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        前端掌握單元測(cè)試-jest

        共 17691字,需瀏覽 36分鐘

         ·

        2022-07-07 18:22

        本文適合對(duì)單元測(cè)試感興趣的小伙伴閱讀

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

        一、前言

        本文基于開源項(xiàng)目:

        https://github.com/facebook/jest

        https://www.jestjs.cn/

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

        二、what Jest

        Jest 是一個(gè)令人愉快的 JavaScript 測(cè)試框架,專注于"簡(jiǎn)潔明快"。
        這些項(xiàng)目都在使用 Jest:Babel、 TypeScript、 Node、 React、 Angular、 Vue 等等!

        特點(diǎn):

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

        三、入門

        安裝 Jest:npm / yarn

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

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

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

        npx jest --init

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

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

          // 指示應(yīng)使用哪個(gè)提供程序來(lái)檢測(cè)代碼以進(jìn)行覆蓋
          coverageProvider"v8",
        };

        Demo

        Tips: 一般單元測(cè)試建議寫在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í)行后會(huì)看到tsconfig.json 文件:

        {
          "compilerOptions": {
            "types": ["node""jest"],
            "target""es2016",                                  
            /* 為發(fā)出的 JavaScript 設(shè)置 JavaScript 語(yǔ)言版本并包含兼容的庫(kù)聲明 */
            "module""commonjs",                                
            /* 指定生成什么模塊代碼. */
           "esModuleInterop"true,                             
           /* 發(fā)出額外的 JavaScript 以簡(jiǎn)化對(duì)導(dǎo)入 CommonJS 模塊的支持。這將啟用 `allowSyntheticDefaultImports` 以實(shí)現(xiàn)類型兼容性. */
            "forceConsistentCasingInFileNames"true,            
            /* 確保imports中的大小寫正確 . */
            "strict"true,                                      
            /* 啟用所有嚴(yán)格的類型檢查選項(xiàng)。 */
            "skipLibCheck"true                                 
            /* 跳過(guò)類型檢查所有 .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,測(cè)試通過(guò)。

        小優(yōu)化

        路徑使用簡(jiǎn)寫,修改 tsconfig.json 配置:

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

        jest.config.js修改moduleNameMapper

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

        五、其他知識(shí)點(diǎn)

        setupFilesAfterEnv 和 setupFiles

        簡(jiǎn)單來(lái)說(shuō):

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

        jsdom 測(cè)試環(huán)境

        jest 提供了 testEnvironment 配置:

        module.exports = {
          testEnvironment"jsdom",
        }

        jsdom: 這個(gè)庫(kù)用 JS 實(shí)現(xiàn)了一套 Node.js 環(huán)境下的 Web 標(biāo)準(zhǔn) API。

        添加 jsdom 測(cè)試環(huán)境后,全局會(huì)自動(dòng)擁有完整的瀏覽器標(biāo)準(zhǔn) API,不需要Mock了。

        引入react/vue

        step1: 安裝Webpack 依賴

        step2: 安裝相應(yīng)的Loader

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

        這里列舉下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)頁(yè)
          devServer: {
            static'./dist',
            port3000,
            hottrue,
          },
          // 默認(rèn)輸出
          output: {
            filename'index.js',
            path: path.resolve(__dirname, 'dist'),
            cleantrue,
          },
          // 指定模板 html
          plugins: [
            new HtmlWebpackPlugin({
              template'./public/index.html',
            }),
          ],
        };

        package.json 添加啟動(dòng)命令

        {
          "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/*"]
            } 
          }
        }

        六、組件測(cè)試

        Demo: 這里列舉了一個(gè)簡(jiǎn)單的場(chǎng)景

        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è)務(wù)組件/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;

        測(cè)試用例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();
          });
        })

        上面這代碼只是一個(gè)簡(jiǎn)單的Demo測(cè)試

        測(cè)試組件功能

        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({
              // 其它的實(shí)現(xiàn)...
              data: { userType"user" },
            });

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

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

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

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

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

        當(dāng)然,我們也可以不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();
        });

        最后測(cè)試用例代碼:

        // 偏向真實(shí)用例
        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 請(qǐng)求", () => {
          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ù),在每個(gè)用例前初始化 Http 請(qǐng)求的 Mock 返回。

        七、小結(jié)

        Jest的功能遠(yuǎn)不止于此,還能做性能測(cè)試、自動(dòng)化測(cè)試等等

        在我們閱讀完官方文檔后,我們一定會(huì)進(jìn)行更深層次的學(xué)習(xí),比如看下框架底層是如何運(yùn)行的,以及源碼的閱讀。

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

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

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

        瀏覽 124
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            孙头退休后日女儿的幸福生活 | 国产高潮女人叫床视频片 | 亚洲AV成人在线 | www.17c久久久嫩草成人 | 亚洲无码成人影院 | 越南妇女毛茸茸高潮 | 国产欧美一区二区三区精华液好吗 | 乱伦小说视频网站 | 毛片在线视频 | 啊┅┅快┅┅用力啊黄蓉猎艳江湖 |