首页  >  文章  >  web前端  >  Jest 简介:单元测试、模拟和异步代码

Jest 简介:单元测试、模拟和异步代码

Linda Hamilton
Linda Hamilton原创
2024-11-01 00:23:28596浏览

Introduction to Jest: Unit Testing, Mocking, and Asynchronous Code

笑话简介

Jest 是一个用于测试 JavaScript 代码的库。

这是一个由 Facebook 维护的开源项目,它特别适合 React 代码测试,但不仅限于此:它可以测试任何 JavaScript 代码。它的优点是:

  • 速度很快
  • 它可以执行快照测试
  • 它固执己见,提供开箱即用的一切,无需您做出选择
export default function sum(a, n) {
  return a + b;
}

divide.test.js

import sum from './sum';

// Describe the test and wrap it in a function.
it('adds 1 + 2 to equal 3', () => {
  const result = sum(1, 2);

  // Jest uses matchers, like pretty much any other JavaScript testing framework.
  // They're designed to be easy to get at a glance;
  // here, you're expecting `result` to be 3.
  expect(result).toBe(3);
});

匹配器

匹配器是一种可以让您测试值的方法。

  • toBe 比较严格相等,使用 ===
  • toEqual 比较两个变量的值。如果它是一个对象或数组,它会检查所有属性或元素的相等性
  • 传递 null 值时 toBeNull 为 true
  • 传递定义值时 toBeDefined 为 true(与上面相反)
  • 传递未定义值时 toBeUndefine 为 true
  • toBeCloseTo 用于比较浮点值,避免舍入错误
  • toBeTruthy true 如果该值被认为是 true (就像 if 那样)
  • toBeFalsy 如果该值被认为是假(就像 if 一样),则为 true
  • 如果 Expect() 的结果高于参数
  • ,则 toBeGreaterThan true
  • toBeGreaterThanOrEqual 如果 Expect() 的结果等于参数或高于参数
  • ,则为 true
  • 如果 Expect() 的结果低于参数
  • ,则 toBeLessThan true
  • toBeLessThanOrEqual 如果 Expect() 的结果等于参数或低于参数
  • ,则为 true
  • toMatch 用于将字符串与正则表达式模式匹配进行比较
  • toContain 用于数组,如果预期数组在其元素集中包含参数
  • ,则为 true
  • toHaveLength(number):检查数组的长度
  • toHaveProperty(key, value):检查对象是否具有属性,并可选择检查其值
  • toThrow 检查您传递的函数是否抛出异常(一般情况下)或特定异常
  • toBeInstanceOf():检查对象是否是类的实例

依赖关系

依赖项是您的应用程序所依赖的一段代码。它可以是我们项目中的函数/对象或第三方依赖项(例如 axios)

当您自己的应用程序没有一段代码就无法运行时,它就成为真正的依赖项。

例如,如果您在应用程序中实现一项功能来发送电子邮件或发出 api 请求或构建配置对象等

两种方法我们可以在js项目的代码中添加依赖项:

进口

export default function sum(a, n) {
  return a + b;
}

依赖注入

只是一个简单概念的奇特术语。

如果您的函数需要外部依赖项的某些功能,只需将其作为参数注入即可。

import sum from './sum';

// Describe the test and wrap it in a function.
it('adds 1 + 2 to equal 3', () => {
  const result = sum(1, 2);

  // Jest uses matchers, like pretty much any other JavaScript testing framework.
  // They're designed to be easy to get at a glance;
  // here, you're expecting `result` to be 3.
  expect(result).toBe(3);
});

单元测试

单元测试由软件开发人员编写和运行,以确保应用程序的一部分(称为“单元”)满足其设计并按预期运行。

我们想要单独测试我们的代码,我们不关心任何依赖项的实际实现。
我们想要验证

  • 我们的代码单元按预期工作
  • 返回预期结果
  • 按其应有的方式调用任何协作者(依赖项)

这就是模拟我们的依赖关系发挥作用的地方。

嘲笑

在单元测试中,模拟为我们提供了存根依赖项所提供的功能的能力,以及意味着观察我们的代码如何与依赖项交互。

当将依赖项直接包含到我们的测试中成本昂贵或不切实际时,例如,当您的代码对 API 进行 HTTP 调用或与数据库层交互时,模拟特别有用。

最好删除这些依赖项的响应,同时确保它们按要求被调用。这就是模拟派上用场的地方。

通过使用模拟函数,我们可以知道以下内容:

  • 收到的来电数量
  • 参数 每次调用时使用的值。
  • 每次调用时的“上下文”或这个值。
  • 函数如何退出以及产生了哪些值

开玩笑地嘲笑

创建模拟函数有多种方法。

  • jest.fn 方法允许我们直接创建一个新的模拟函数。
  • 如果您正在模拟对象方法,则可以使用 jest.spyOn。
  • 如果你想模拟整个模块,你可以使用 jest.mock。

jest.fn 方法本身就是一个高阶函数。

这是一个工厂方法,用于创建新的、未使用的模拟函数。

JavaScript 中的函数是一等公民,它们可以作为参数传递。

每个模拟函数都有一些特殊的属性。模拟属性是基础。此属性是一个对象,其中包含有关如何调用函数的所有模拟状态信息。该对象包含三个数组属性:

  • 调用 [每次调用的参数]
  • 实例 [每次调用时的“this”值]
  • Results [函数退出的值],results 属性有类型(return 或 throw)和值
    • 函数显式返回一个值。
    • 函数运行完成,没有 return 语句(相当于返回 undefined
    • 函数抛出错误。
export default function sum(a, n) {
  return a + b;
}
  • https://codesandbox.io/s/implementing-mock-functions-tkc8b

模拟基础

import sum from './sum';

// Describe the test and wrap it in a function.
it('adds 1 + 2 to equal 3', () => {
  const result = sum(1, 2);

  // Jest uses matchers, like pretty much any other JavaScript testing framework.
  // They're designed to be easy to get at a glance;
  // here, you're expecting `result` to be 3.
  expect(result).toBe(3);
});

模拟注入的依赖项

import { name, draw, reportArea, reportPerimeter } from './modules/square.js';

模拟模块

使用 jest.fn 模拟函数

// Constructor Injection

// DatabaseManager class takes a database connector as a dependency
class DatabaseManager {
    constructor(databaseConnector) {
        // Dependency injection of the database connector
        this.databaseConnector = databaseConnector;
    }

    updateRow(rowId, data) {
        // Use the injected database connector to perform the update
        this.databaseConnector.update(rowId, data);
    }
}

// parameter injection, takes a database connector instance in as an argument; easy to test!
function updateRow(rowId, data, databaseConnector) {
    databaseConnector.update(rowId, data);
}

这种类型的嘲笑不太常见,原因如下:

  • jest.mock 自动为模块中的所有函数执行此操作
  • jest.spyOn 做同样的事情,但允许恢复原始功能

使用 jest.mock 模拟模块

更常见的方法是使用 jest.mock 自动将模块的所有导出设置为 Mock 函数

// 1. The mock function factory
function fn(impl = () => {}) {
  // 2. The mock function
  const mockFn = function(...args) {
    // 4. Store the arguments used
    mockFn.mock.calls.push(args);
    mockFn.mock.instances.push(this);
    try {
      const value = impl.apply(this, args); // call impl, passing the right this
      mockFn.mock.results.push({ type: 'return', value });
      return value; // return the value
    } catch (value) {
      mockFn.mock.results.push({ type: 'throw', value });
      throw value; // re-throw the error
    }
  }
  // 3. Mock state
  mockFn.mock = { calls: [], instances: [], results: [] };
  return mockFn;
}

使用 jest.spyOn 监视或模拟函数

有时您只想观看一个方法被调用,但保留原始实现。其他时候,您可能想要模拟实现,但稍后在套件中恢复原始版本。

test("returns undefined by default", () => {
  const mock = jest.fn();

  let result = mock("foo");

  expect(result).toBeUndefined();
  expect(mock).toHaveBeenCalled();
  expect(mock).toHaveBeenCalledTimes(1);
  expect(mock).toHaveBeenCalledWith("foo");
});

恢复原来的实现

const doAdd = (a, b, callback) => {
  callback(a + b);
};

test("calls callback with arguments added", () => {
  const mockCallback = jest.fn();
  doAdd(1, 2, mockCallback);
  expect(mockCallback).toHaveBeenCalledWith(3);
});

JavaScript 和事件循环

JavaScript 是单线程的: 一次只能运行一个任务。通常这没什么大不了的,但现在想象一下你正在运行一个需要 30 秒的任务.. 是的.. 在该任务期间,我们等待 30 秒,然后才会发生其他事情(JavaScript 默认在浏览器的主线程上运行,所以整个UI都卡住了)。
现在是 2020 年,没有人想要一个缓慢、反应迟钝的网站。

幸运的是,浏览器为我们提供了一些 JavaScript 引擎本身不提供的功能:Web API。这包括 DOM APIsetTimeoutHTTP 请求 等。这可以帮助我们创建一些异步非阻塞行为

export default function sum(a, n) {
  return a + b;
}
  • 调用堆栈 - 当我们调用一个函数时,它会被添加到称为调用堆栈的东西中。
  • WebAPI - setTimeout 由 WebAPI 提供,采用回调函数并设置计时器,而不阻塞主线程
  • 队列 - 当计时器结束时,回调被添加到队列
  • 事件循环 - 检查调用堆栈是否为空,检查队列中是否有要执行的回调,然后移至要执行的调用堆栈
import sum from './sum';

// Describe the test and wrap it in a function.
it('adds 1 + 2 to equal 3', () => {
  const result = sum(1, 2);

  // Jest uses matchers, like pretty much any other JavaScript testing framework.
  // They're designed to be easy to get at a glance;
  // here, you're expecting `result` to be 3.
  expect(result).toBe(3);
});

使用 Jest 测试异步代码

Jest 通常希望同步执行测试的函数

如果我们执行异步操作,但我们不让 Jest 知道它应该等待测试结束,则会给出误报。

import { name, draw, reportArea, reportPerimeter } from './modules/square.js';

异步模式
JavaScript 中有多种处理异步操作的模式;最常用的是:

  • 回调
  • Promise 和异步/等待

测试回调

你不能在回调中进行测试,因为 Jest 不会执行它 - 测试文件的执行在回调被调用之前结束。要解决此问题,请将参数传递给测试函数,您可以方便地调用“done”。 Jest 将等到您调用 done() 后再结束测试:

// Constructor Injection

// DatabaseManager class takes a database connector as a dependency
class DatabaseManager {
    constructor(databaseConnector) {
        // Dependency injection of the database connector
        this.databaseConnector = databaseConnector;
    }

    updateRow(rowId, data) {
        // Use the injected database connector to perform the update
        this.databaseConnector.update(rowId, data);
    }
}

// parameter injection, takes a database connector instance in as an argument; easy to test!
function updateRow(rowId, data, databaseConnector) {
    databaseConnector.update(rowId, data);
}

承诺

对于返回 Promise 的函数,我们从测试中返回一个 Promise:

// 1. The mock function factory
function fn(impl = () => {}) {
  // 2. The mock function
  const mockFn = function(...args) {
    // 4. Store the arguments used
    mockFn.mock.calls.push(args);
    mockFn.mock.instances.push(this);
    try {
      const value = impl.apply(this, args); // call impl, passing the right this
      mockFn.mock.results.push({ type: 'return', value });
      return value; // return the value
    } catch (value) {
      mockFn.mock.results.push({ type: 'throw', value });
      throw value; // re-throw the error
    }
  }
  // 3. Mock state
  mockFn.mock = { calls: [], instances: [], results: [] };
  return mockFn;
}

异步/等待

为了测试返回 Promise 的函数,我们还可以使用 async/await,这使得语法非常简单明了:

test("returns undefined by default", () => {
  const mock = jest.fn();

  let result = mock("foo");

  expect(result).toBeUndefined();
  expect(mock).toHaveBeenCalled();
  expect(mock).toHaveBeenCalledTimes(1);
  expect(mock).toHaveBeenCalledWith("foo");
});

尖端

  • 我们需要很好地理解我们的函数的作用以及我们要测试的内容
  • 思考我们正在测试的代码的行为
  • 设置舞台:
    • 模拟/监视任何依赖项
    • 我们的代码是否与全局对象交互?我们也可以嘲笑/监视他们
    • 我们的测试是否与 DOM 交互?我们可以构建一些假元素来使用
    • 构建你的测试
    • 鉴于...
    • 当我打电话时......
    • 然后...我期待......
const doAdd = (a, b, callback) => {
  callback(a + b);
};

test("calls callback with arguments added", () => {
  const mockCallback = jest.fn();
  doAdd(1, 2, mockCallback);
  expect(mockCallback).toHaveBeenCalledWith(3);
});

链接

  • https://medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c
  • https://jestjs.io/docs/en/mock-functions
  • https://codesandbox.io/s/implementing-mock-functions-tkc8b
  • https://github.com/BulbEnergy/jest-mock-examples
  • https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif
  • https://jestjs.io/docs/en/asynchronous
  • https://www.pluralsight.com/guides/test-asynchronous-code-jest

以上是Jest 简介:单元测试、模拟和异步代码的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn