初识 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.fn 的 mock 函数来测试

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 目录下的 js、jsx、ts、tsx 文件,但是排除 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"];
    }
相关推荐
2603_965896629 分钟前
Grid 网格布局|CSS 二维页面排版体系
前端·css
有味道的男人25 分钟前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
lovingsoft1 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly1 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝1 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦1 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡2 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI2 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95272 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人2 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring