初识 jest

jest

jest 需要配合 git 使用,因为 jest 会检测 git 的提交记录,然后运行相关的测试用例。

运行 jest --watch 时,终端会有一下提示:

js 复制代码
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
  • a 运行所有测试用例
  • f 只运行失败的测试用例,如果一开始成功,后来改失败了,不会运行
  • p 用正则匹配文件名,只运行匹配到这个文件名的测试用例
  • t 用正则匹配测试用例名,只运行匹配到这个名字的测试用例

异步测试

  1. 对于异步函数 fetchData 的测试,jest 默认不会等待异步测试用例执行完毕,所以需要在测试用例中传入一个 done 函数,然后在异步操作完成后调用这个函数

    go 复制代码
    const fetchData = (fn) => {
      fetch("http://localhost:8080/ping").then(async (response) => {
        if (response.ok) {
          const res = await response.json();
          fn(res);
        } else {
          fn(response.status);
        }
      });
    };
    
    test("fetchData", (done) => {
      fetchData((data) => {
        expect(data).toEqual({ message: "pong" });
        done();
      });
    });
  2. 修改异步函数 fetchData,返回一个 Promise 对象

    js 复制代码
    const fetchData = () => {
      return new Promise((resolve, reject) => {
        fetch("http://localhost:8080/ping").then(async (response) => {
          if (response.ok) {
            resolve(await response.json());
          } else {
            reject(response.status);
          }
        });
      });
    };
    test("fetchData", () => {
      // 需要将 return 的 Promise 对象返回,否则 jest 会认为测试用例已经执行完毕
      return fetchData().then((data) => {
        expect(data).toEqual({ message: "pong" });
      });
    });
  3. 测试 reject 报错的情况,通过 catch 方法捕获错误,需要在测试用例中添加 expect.assertions(1),因为异步请求响应正确的话 catch 分支不会被执行

    js 复制代码
    test("fetchData", () => {
      expect.assertions(1);
      return fetchData().catch((data) => {
        expect(data).toEqual(404);
      });
    });
  4. 通过 toMatchObject 方法判断

    js 复制代码
    test("fetchData", () => {
      return expect(fetchData()).resolves.toMatchObject({
        message: "pong",
      });
    });
  5. 测试 reject 报错情况

    js 复制代码
    test("fetchData", () => {
      // 如果抛出的是 throw new Error(),则使用 toThrow
      // return expect(fetchData()).rejects.toThrow();
      return expect(fetchData()).rejects.toEqual(404);
    });

mock

现在有个函数接收一个回调函数,并执行这个函数,测试用例应该怎么写呢?

js 复制代码
const callback = (cb) => {
  cb();
};

jest 提供了一个 jest.fnmock 函数来测试

js 复制代码
test("mock", () => {
  const mockFn = jest.fn();
  callback(mockFn);
  expect(mockFn).toBeCalled();
});

mock 更高级的用法是,可以模拟请求的响应,在实际的测试中,我们不需要等待网络请求的响应,只需要模拟一个请求的响应即可

如果项目中使用 fetch 发起网络请求,需要使用下面的方法替代 fetch

js 复制代码
global.fetch = jest.fn();

然后在测试用例中使用 mockImplementationOnce 或者 mockResolvedValue 模拟请求的响应

  • mockImplementationOnce

    js 复制代码
    fetch.mockImplementationOnce(() =>
      Promise.resolve({
        json: () => Promise.resolve({ message: "pong" }),
        text: () => Promise.resolve("mocked text"),
        ok: true,
        status: 200,
        statusText: "ok",
      })
    );
  • mockResolvedValue

    js 复制代码
    fetch.mockResolvedValue(
      Promise.resolve({
        json: () => Promise.resolve({ message: "pong" }),
        text: () => Promise.resolve("mocked text"),
        ok: true,
        status: 200,
        statusText: "ok",
      })
    );
js 复制代码
test("mock", async () => {
  fetch.mockImplementationOnce(() =>
    Promise.resolve({
      json: () => Promise.resolve({ message: "pong" }),
      text: () => Promise.resolve("mocked text"),
      ok: true,
      status: 200,
      statusText: "ok",
    })
  );

  await fetchData().then((res) => {
    expect(res).toEqual({ message: "pong" });
  });
});

mock 高级用法

在当前目录下新建 __mocks__ 目录

新建文件 fetchData.js,写一个 fetchData 函数,然后在测试用例中引入这个函数

js 复制代码
export const fetchData = () => {
  return Promise.resolve({ message: "pong" });
};

在文件的开头用 jest.mock 引入这个文件,jest.mock 会自动到 __mocks__ 目录下找对应的文件(可以通过配置文件 jest.config.js 属性 automock 来配置)

import 导入的函数就会去 __mocks__ 目录下找对应的文件

js 复制代码
jest.mock("./fetchData.js");
import { fetchData } from "./fetchData";
test("fetchData", () => {
  return fetchData().then((data) => {
    expect(data).toEqual({ message: "pong" });
  });
});

如果想要引入真实 ./fetchData.js 文件中的函数,可以使用 jest.requireActual 方法

js 复制代码
const {} = jest.requireActual("./fetchData.js");

snapshot

jest 提供了 snapshot 功能,可以将测试用例的结果保存到快照文件中,然后在下次测试时,将测试用例的结果与快照文件中的结果进行比较,如果不一致,则测试用例失败

jest 会在项目的根目下生成一个 __snapshots__ 文件夹,里面存放着测试用例的快照文件

测试不通过的话,按 u 键更新所有测试用例的快照文件,按 i 键一个个更新测试用例的快照文件

js 复制代码
test("snapshot", () => {
  expect(generateConfig()).toMatchSnapshot();
});

如果是个动态的数据,可以给 toMatchSnapshot 传入一个参数

js 复制代码
expect(generateConfig()).toMatchSnapshot({
  time: expect.any(Date),
});

定时器

定义一个 setTimeout 定时器,每隔 3 秒执行一次回调函数

js 复制代码
export const timer = (cb) => {
  setTimeout(() => {
    cb();
    setTimeout(() => {
      cb();
    }, 3000);
  }, 3000);
};

我们在测试时,不需要等待 3 秒,可以使用 jest 提供的 jest.advanceTimersByTime 方法,来快速执行定时器

js 复制代码
jest.useFakeTimers();

test("timer", () => {
  const mockFn = jest.fn();
  timer(mockFn);
  jest.advanceTimersByTime(6000); // 快速执行 2 个定时器
  expect(mockFn).toHaveBeenCalledTimes(2);
});

test("timer", () => {
  const mockFn = jest.fn();
  timer(mockFn);
  jest.advanceTimersByTime(3000); // 快速执行 1 个定时器
  expect(mockFn).toHaveBeenCalledTimes(1);
  jest.advanceTimersByTime(3000); // 再快速执行 1 个定时器
  expect(mockFn).toHaveBeenCalledTimes(2);
});

jest 还有两个方法可以快速执行定时器:

  • jest.runAllTimers 方法可以快速执行所有定时器
  • jest.runOnlyPendingTimers 方法可以快速执行队列中的待执行的定时器(只执行第一层的 setTimeout

jest 配置

  • collectCoverageFrom 配置需要收集测试覆盖率的文件

    • 表示分析 src 目录下的 jsjsxtstsx 文件,但是排除 src 目录下的 .d.ts 文件
    js 复制代码
    {
      collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"];
    }
  • setupFiles 配置在测试用例运行之前需要运行的文件

    js 复制代码
    {
      setupFiles: ["react-app-polyfill/jsdom"];
    }
  • setupFilesAfterEnv 配置在测试用例运行之后需要做一些其他事情

    js 复制代码
    {
      setupFilesAfterEnv: ["<rootDir>/src/setupTests.js"];
    }
  • testMatch 配置测试文件的匹配规则

    js 复制代码
    {
      testMatch: ["<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"];
    }
  • testEnvironment 配置测试环境

    js 复制代码
    {
      testEnvironment: "jsdom";
    }
  • transform 配置需要转换的文件

    • 当以 .js.jsx.mjs.cjs.ts.tsx 结尾时,使用 babelTransform.js 文件进行转换
    js 复制代码
    {
      transform: {
        "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
        "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
        "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
      };
    }
  • transformIgnorePatterns 配置不需要转换的文件

    js 复制代码
    {
      transformIgnorePatterns: [
        "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
        "^.+\\.module\\.(css|sass|scss)$",
      ];
    }
  • modulePaths 配置模块路径

  • moduleNameMapper 配置模块映射

    • 当引入 css module 这样的文件时,identity-obj-proxy 会返回一个对象,这个对象的属性名和属性值是一样的
    js 复制代码
    {
      moduleNameMapper: {
        "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
      },
    };
  • moduleFileExtensions 配置模块文件扩展名

    js 复制代码
    {
      moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json", "node"];
    }
  • watchPlugins 配置监听插件

    js 复制代码
    {
      watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"];
    }
相关推荐
你不讲 wood3 分钟前
组件通信——provide 和 inject 实现爷孙组件通信
前端·javascript·vue.js
我码玄黄10 分钟前
高效Flutter应用开发:GetX状态管理实战技巧
前端·flutter·状态管理
codeMing_15 分钟前
Vue使用query传参Boolean类型,刷新之后转换为String问题
前端·javascript·vue.js
神仙别闹37 分钟前
基于Java+Mysql实现(WEB)宿舍管理系统
java·前端·mysql
有一个好名字1 小时前
后端Controller获取成功,但是前端报错404
前端·spring
stpzhf1 小时前
记录特别代码样式
前端·javascript·css
Jet_closer_burning1 小时前
css grid布局属性详解
前端·css·html
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_026】4.4 Flexbox 元素对齐、间距等细节处理(上)
前端·css·css3·html5·flexbox·css布局
本郡主是喵1 小时前
由于安装nvm 引发的vue : 无法将“vue”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。
前端·javascript·vue.js
京城五1 小时前
text-overflow:ellipsis 不生效的情况解决办法
前端·css·html