w3ctech

[Jest]单元测试初学者指南 - 第三部分 - 模拟Http请求和访问文件

原文: Unit Testing Beginners Guide - Part 3 - Mock Http And Files access

作者:jstweetster

示例代码

在本文结束时,您将能够使用Jest正确的测试包含http请求,文件访问9号彩票方法 ,数据库调用或者任何其他类型的辅助作用的生产代码。此外,您将学习如何处理其他类似的问题,而且您需要避免实际调用的9号彩票方法 活模块(例如数据库调用)。

译者注:接下来的测试涉及到Jest需要使用Babel的情况,所以需要安装一些依赖,可以查看官方文档说明 https://jestjs.io/docs/zh-Hans/getting-started ,也可以直接查看上面给出的示例代码。

1)安装依赖

yarn add --dev babel-jest babel-core regenerator-runtime babel-preset-env # 也可以使用 npm 安装

2)在项目根目录新建 .babelrc 文件,添加内容如下:

{
  "presets": ["env"]
}

模拟HTTP请求

让9号彩票9号彩票我 们 假设9号彩票9号彩票我 们 有一段代码使用 XMLHttpRequest 9号彩票方法 执行网络请求,如下:

const API_ROOT = 'http://jsonplaceholder.typicode.com';
class API {
    getPosts() {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', `${API_ROOT}/posts`);
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    const resp = JSON.parse(xhr.responseText);
                    if (resp.error) {
                        reject(resp.error);
                    } else {
                        resolve(resp);
                    }
                }
            }
            xhr.send();
        })
    }
}

export default new API();

9号彩票9号彩票我 们 怎样测试这个9号彩票方法 ?

首先,让9号彩票9号彩票我 们 定义这个9号彩票方法 是干什么的。

getPosts() 使用API调用来检索博客中的帖子列表,并且返回一个解析帖子列表的 Promise

9号彩票9号彩票我 们 想要避免的是在9号彩票9号彩票我 们 的测试中实际进行API调用。

为什么?

  • 因为调用的资源可能在运行测试的环境中不可用
  • 因为9号彩票9号彩票我 们 有第三方依赖(API端点),这是的测试容易失败。
  • 因为测试将会运行的很慢
  • 因为端点调用可能具有需要清理的意外后果(例如写入DB)
  • 因为9号彩票你 不再进行单元测试了。相反,9号彩票9号彩票我 们 将进行功能测试领域了
  • 最后,因为被调用的资源可能在时间和复杂性方面执行昂贵的操作

正如9号彩票你 所看到的,9号彩票9号彩票我 们 有很多理由希望避免直接在具有实际网络请求单元测试中工作。

模拟 XMLHttpRequest

解决9号彩票方法 是回溯到使用 XMLHttpRequest 对象的正确模拟,拦截对它的调用并伪造行为。

如果9号彩票你 不记得究竟模拟什么和怎样使用,9号彩票你 可以查看“使用Jest中的spies和fake timers” 和Jest官方文档对mocks的说明。

让9号彩票9号彩票我 们 尝试创建第一个测试(假设9号彩票9号彩票我 们 有一个 api.spec.js 文件):

import API from '../src/api.js';
const mockXHR = {
  open: jest.fn(),
  send: jest.fn(),
  readyState: 4,
  responseText: JSON.stringify(
    [
      {
        title: 'test post'
      },
      {
        tile: 'second test post'
      }
    ]
  )
};
const oldXMLHttpRequest = 9号彩票Win
dow.XMLHttpRequest;
9号彩票Win
dow.XMLHttpRequest = jest.fn(() => mockXHR);

describe('API integration test suite', function () {
  test('Should retrieve the list of posts from the server when calling getPosts method', function (done) {
    const reqPromise = API.getPosts();
    mockXHR.onreadystatechange();
    reqPromise.then((posts) => {
      expect(posts.length).toBe(2);
      expect(posts[0].title).toBe('test post');
      expect(posts[1].title).toBe('second test post');
      done();
    });
  });
});

让9号彩票9号彩票我 们 一步一步的来了解发生了什么

const mockXHR = {
  open: jest.fn(),
  send: jest.fn(),
  readyState: 4,
  responseText: JSON.stringify(
    [
      {
        title: 'test post'
      },
      {
        title: 'second test post'
      }
    ]
  )
};

9号彩票9号彩票我 们 开始创建了一个假的XHR对象,实际的 opensend 9号彩票方法 是不做任何事情的函数。9号彩票9号彩票我 们 还设置 readyState 的值为4(通常用于检测请求是否已完成)和 responseText 的值为适合9号彩票9号彩票我 们 要测试的内容。

9号彩票9号彩票我 们 可以通过为 responseText 提供正确的文本值来模拟9号彩票9号彩票我 们 想要的任何API响应。

const oldXMLHttpRequest = 9号彩票Win
dow.XMLHttpRequest;
9号彩票Win
dow.XMLHttpRequest = jest.fn(() => mockXHR);

接下来,9号彩票9号彩票我 们 将备份内置的XMLHttpRequest 对象,将其替换为返回9号彩票9号彩票我 们 的模拟对象的函数。备份真正的 XMLHttpRequest 对象是一个好主意,因为在测试结束时9号彩票9号彩票我 们 应该清理环境,让环境处于使用它之前的初始状态。

因此,每当调用 new XMLHttpRequest 时,将会返回 mockXHR 对象。

而且,最后9号彩票9号彩票我 们 使用这个设置,对 getPosts 函数进行单元测试更加容易啦。

const reqPromise = API.getPosts();
mockXHR.onreadystatechange();

调用这个 API ,然后通过调用 onreadystatechange 函数来模拟响应到达。当 onreadystatechange 被调用后,9号彩票9号彩票我 们 实际上正在调用在 api.js 中设置的状态更改的回调函数:

xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
        const resp = JSON.parse(xhr.responseText);
        if (resp.error) {
            reject(resp.error);
        } else {
            resolve(resp);
        }
    }
}

因为模拟的 XHR 对象的 readyState 的值设置为 4,Promise 对象将会立即被解析(resolved)或拒绝(rejected),具体取决于响应。

根据断言,9号彩票9号彩票我 们 验证从 getPosts() 函数返回的 Promise 对象是否具有实际的 JSON 响应值作为被解析的值。

9号彩票9号彩票我 们 通过检查文章数量是否正确以及每篇文章应该是什么值来验证测试的正确性。

expect(posts.length).toBe(2);
expect(posts[0].title).toBe('test post');
expect(posts[1].title).toBe('second test post');

深入理解

9号彩票9号彩票我 们 可以将该9号彩票方法 进行一些改进,使其更灵活和易于使用。

首先,让9号彩票9号彩票我 们 创建一个可以模拟 XHR 对象的工厂函数,它能够让9号彩票9号彩票我 们 更加容易的创建新的模拟 XHR,并且,可选择的指定响应的对象。

const createMockXHR = (responseJSON) => {
    const mockXHR = {
        open: jest.fn(),
        send: jest.fn(),
        readyState: 4,
        responseText: JSON.stringify(
            responseJSON || {}
        )
    };
    return mockXHR;
}

接下来,让9号彩票9号彩票我 们 为每一个单元测试创建一个新的 XHR 对象。在单元测试时,9号彩票9号彩票我 们 最不希望的是在单元测试中具有共享状态,这会导致不可预测且难以调试的测试。实际的测试单元如下:

describe('API integration test suite', function() {
  const oldXMLHttpRequest = 9号彩票Win
dow.XMLHttpRequest;
  let mockXHR = null;

  beforeEach(() => {
    mockXHR = createMockXHR();
    9号彩票Win
dow.XMLHttpRequest = jest.fn(() => mockXHR);
  });

  afterEach(() => {
    9号彩票Win
dow.XMLHttpRequest = oldXMLHttpRequest;
  });

  test('Should retrieve the list of posts from the server when calling getPosts method', function(done) {
    const reqPromise = API.getPosts();
    mockXHR.responseText = JSON.stringify([
      { title: 'test post' },
      { title: 'second test post' }
    ]);
    mockXHR.onreadystatechange();
    reqPromise.then((posts) => {
      expect(posts.length).toBe(2);
      expect(posts[0].title).toBe('test post');
      expect(posts[1].title).toBe('second test post');
      done();
    });
  });
});

此外,在每次测试之后,9号彩票9号彩票我 们 通过在 beforeEach 中模拟XMLHttpRequest 并在 afterEach 中恢复原本的 XMLHttpRequest 对象来清理环境。

  beforeEach(() => {
    mockXHR = createMockXHR();
    9号彩票Win
dow.XMLHttpRequest = jest.fn(() => mockXHR);
  });

  afterEach(() => {
    9号彩票Win
dow.XMLHttpRequest = oldXMLHttpRequest;
  });

9号彩票9号彩票我 们 获得的另一个好处是9号彩票9号彩票我 们 可以非常轻松的测试不同的场景。假设9号彩票9号彩票我 们 想要添加另一个测试,模拟 API 返回错误:

test('Should return a failed promise with the error message when the API returns an error', function(done) {
    const reqPromise = API.getPosts();
    mockXHR.responseText = JSON.stringify({
      error: 'Failed to GET posts'
    });
    mockXHR.onreadystatechange();
    reqPromise.catch((err) => {
      expect(err).toBe('Failed to GET posts');
      done();
    });
  });

9号彩票你 是否注意到模拟API不同的响应变得更加容易啦?

如何模拟文件系统/数据库调用和其他副作用(side effects)

9号彩票9号彩票我 们 可以非常轻松地扩展9号彩票9号彩票我 们 应用于HTTP请求的9号彩票技术 ,以涵盖其他类型的副作用。

假设9号彩票9号彩票我 们 有一个 FileSystem 组件,它有一个读取文件并将其解析为 JSON 的9号彩票方法 :

import fs from 'fs';
export default class FileSystem {
  parseJSONFile(file) {
    const content = String(fs.readFileSync(file));
    return JSON.parse(content);
  }
}

9号彩票9号彩票我 们 想要测试 parseJSONFile() 9号彩票方法 ,但是9号彩票9号彩票我 们 也想要避免从磁盘中创建文件和读取它的内容。

9号彩票9号彩票我 们 的测试单元如下:

jest.mock('fs', () => ({
  readFileSync: jest.fn()
}));

import FileSystem from './FileSystem.js';
import fs from 'fs';

describe('FileSystem test suite', function() {
  test('Should return the parsed JSON from a file specified as param', function(done) {
    const fileReader = new FileSystem();
    fs.readFileSync.mockReturnValue('{ "test": 1 }');
    const result = fileReader.parseJSONFile('test.json');
    expect(result).toEqual({ "test": 1 });
    done();
  });
});

让9号彩票9号彩票我 们 一步一步的来看:

jest.mock('fs', () => ({
    readFileSync: jest.fn()
})})

jest.mock 允许9号彩票9号彩票我 们 模拟9号彩票9号彩票我 们 可能拥有的任何模块,包括在 NodeJS 中构建的并有工厂函数的模块作为第二个参数 (arg),返回模拟的返回值。

在9号彩票9号彩票我 们 的示例中,每当9号彩票9号彩票我 们 的代码里有 const fs = require('fs'); 或者 import fs from 'fs' ,导入的值实际上是9号彩票9号彩票我 们 从工厂函数返回的对象:

{
    readFileSync: jest.fn()
}

在使用 fs.readFileSync 之前调用 jest.mock 很重要。

接下来,9号彩票9号彩票我 们 实例化9号彩票9号彩票我 们 要测试的组件 const fileReader = new FileSystem(); ,并指示 readFileSync spy 返回某个预先制作的字符串:

fs.readFileSync.mockRetrunValue('{ "test": 1 }');

如果9号彩票你 想要了解9号彩票更多 的操作信息,请在 Jest 文档中查看 mockReturnValue

最后,9号彩票9号彩票我 们 验证 parseJSONFile 的结果是解析的 JSON 值:

expect(result).toEqual({ "test": 1 });

以上是对模拟http调用和文件系统调用的介绍。

w3ctech微信

扫码关注w3ctech微信9号彩票公众号

共收到0条回复