使用Mocks在Jest中进行JavaScript测试

Using Mocks for Testing in JavaScript with Jest

介绍

Jest是一种流行的JavaScript开放源代码测试框架。 我们可以使用Jest在测试中创建模拟-在测试代码时替换对象的对象。

在之前的有关使用Sinon.js的单元测试技术的系列文章中,我们介绍了如何使用Sinon.js存根,监视和模拟Node.js应用程序-特别是HTTP调用。

在本系列中,我们将使用Jest涵盖Node.js中的单元测试技术。 Jest由Facebook创建,并与React,Angular和Vue等许多JavaScript库和框架很好地集成在一起。 它特别关注简单性和性能。

在本文中,我们将回顾什么是模拟,然后重点介绍如何在测试中为Node.js应用程序设置Jest以模拟HTTP调用。 然后,我们将比较如何使用Jest和Sinon为程序创建模拟。

什么是Mo子?

在单元测试中,模拟为我们提供了对依赖项提供的功能进行存根的功能,并提供了一种观察代码如何与依赖项进行交互的方法。 当将依赖直接包含在我们的测试中非常昂贵或不切实际时,例如在您的代码正在对API进行HTTP调用或与数据库层进行交互的情况下,模拟是非常有用的。

最好为这些依赖项存根响应,同时确保按需调用它们。 这是模拟派上用场的地方。

现在让我们看看如何使用Jest在Node.js中创建模拟。

在Node.js应用程序中设置Jest

在本教程中,我们将设置一个Node.js应用程序,该应用程序将对包含相册中的照片的JSON API进行HTTP调用。 在我们的测试中,将使用Jest模拟API调用。

首先,让我们创建文件将驻留并移动到的目录:

1
$ mkdir PhotoAlbumJest && cd PhotoAlbumJest

然后,让我们使用默认设置初始化Node项目:

1
$ npm init -y

项目初始化后,我们将继续在目录的根目录下创建index.js文件:

1
$ touch index.js

为了帮助我们处理HTTP请求,我们将使用Axios。

设置Axios

我们将使用axios作为我们的HTTP客户端。 Axios是Node.js的轻量级,基于承诺的HTTP客户端,Web浏览器也可以使用它。 这使其非常适合我们的用例。

首先安装它:

1
$ npm i axios --save

在使用axios之前,我们将创建一个名为axiosConfig.js的文件,通过它我们将配置Axios客户端。 配置客户端可以使我们在一组HTTP请求中使用通用设置。

例如,我们可以为一组HTTP请求(最常见的是为所有HTTP请求使用的基本URL)设置授权标头。

让我们创建配置文件:

1
touch axiosConfig.js

现在,让我们访问axios并配置基本URL:

1
2
3
4
5
6
7
const axios = require('axios');

const axiosInstance = axios.default.create({
    baseURL: 'https://jsonplaceholder.typicode.com/albums'
});

module.exports = axiosInstance;

设置baseURL之后,我们导出了axios实例,以便可以在我们的应用程序中使用它。 我们将使用www.jsonplaceholder.typicode.com,它是用于测试和原型制作的伪造在线REST API。

在我们之前创建的index.js文件中,让我们定义一个函数,该函数返回给定相册ID的照片URL列表:

1
2
3
4
5
6
7
8
9
10
11
12
const axios = require('./axiosConfig');

const getPhotosByAlbumId = async (id) => {
    const result = await axios.request({
        method: 'get',
        url: `/${id}/photos?_limit=3`
    });
    const { data } = result;
    return data;
};

module.exports = getPhotosByAlbumId;

要使用我们的API,我们只需使用axios实例的axios.request()方法。 我们传入方法的名称,在本例中为get和要调用的url

我们传递给url字段的字符串将从axiosConfig.js连接到baseURL

现在,让我们为此功能设置一个Jest测试。

设置笑话

要设置Jest,我们必须首先使用npm将Jest安装为开发依赖项:

1
$ npm i jest -D

-D标志是--save-dev的快捷方式,它告诉NPM将其保存为开发依赖项。

然后,我们将继续为Jest创建一个名为jest.config.js的配置文件:

1
touch jest.config.js

现在,在jest.config.js文件中,我们将设置测试将驻留的目录:

1
2
3
4
5
6
7
module.exports = {
    testMatch: [
        '<rootDir>/**/__tests__/**/?(*.)(spec|test).js',
        '<rootDir>/**/?(*.)(spec|test).js'
    ],
    testEnvironment: 'node',
};

testMatch值是Jest将用来检测测试文件的全局模式数组。 在本例中,我们指定__tests__目录内或项目中任何扩展名为.spec.js或。test.js的任何文件都应视为测试文件。

注意:在JavaScript中,通常会看到测试文件以.spec.js结尾。 开发人员使用"规格"作为"规格"的简写。 这意味着测试包含正在实施的功能的功能要求或规格。

testEnvironment值表示运行Jest的环境,即是在Node.js中还是在浏览器中。 您可以在此处阅读有关其他允许的配置选项的更多信息。

现在,让我们修改package.json测试脚本,以使其使用Jest作为我们的测试运行器:

1
2
3
"scripts": {
 "test":"jest"
},

我们的设置完成。 要测试我们的配置是否有效,请在名为index.spec.js的目录的根目录下创建一个测试文件:

1
touch index.spec.js

现在,在文件中,让我们编写一个测试:

1
2
3
4
5
describe('sum of 2 numbers', () => {
    it(' 2 + 2 equal 4', () => {
        expect(2 + 2).toEqual(4)
    });
});

使用以下命令运行此代码:

1
$ npm test

您应该得到以下结果:

1
2
3
4
5
6
7
8
9
 PASS  ./index.spec.js
  sum of 2 numbers
    ? 2 + 2 equal 4 (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.897s, estimated 1s
Ran all test suites.

正确设置了Jest之后,我们现在可以继续编写代码以模拟我们的HTTP调用。

嘲笑HTTP呼叫

index.spec.js文件中,我们将重新开始,删除旧代码并编写新的脚本来模拟HTTP调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const axios = require('./axiosConfig');
const getPhotosByAlbumId = require('./index');

jest.mock('./axiosConfig', () => {
    return {
        baseURL: 'https://jsonplaceholder.typicode.com/albums',
        request: jest.fn().mockResolvedValue({
            data: [
                {
                    albumId: 3,
                    id: 101,
                    title: 'incidunt alias vel enim',
                    url: 'https://via.placeholder.com/600/e743b',
                    thumbnailUrl: 'https://via.placeholder.com/150/e743b'
                },
                {
                    albumId: 3,
                    id: 102,
                    title: 'eaque iste corporis tempora vero distinctio consequuntur nisi nesciunt',
                    url: 'https://via.placeholder.com/600/a393af',
                    thumbnailUrl: 'https://via.placeholder.com/150/a393af'
                },
                {
                    albumId: 3,
                    id: 103,
                    title: 'et eius nisi in ut reprehenderit labore eum',
                    url: 'https://via.placeholder.com/600/35cedf',
                    thumbnailUrl: 'https://via.placeholder.com/150/35cedf'
                }
            ]
        }),
    }
});

在这里,我们首先使用require语法导入依赖项。 由于我们不想进行任何实际的网络调用,因此我们使用jest.mock()方法为axiosConfig创建了一个手动模拟。 jest.mock()方法将模块路径作为参数,并将模块的可选实现作为工厂参数。

对于factory参数,我们指定模拟axiosConfig应该返回由baseURLrequest()组成的对象。 baseURL设置为API的基本URL。 request()是一个模拟函数,它返回照片数组。

我们在此处定义的request()函数替代了实际的axios.request()函数。 当我们调用request()方法时,将改为调用我们的模拟方法。

重要的是jest.fn()函数。 它返回一个新的模拟函数,并在括号内定义其实现。 我们通过mockResolvedValue()函数完成的工作为request()函数提供了新的实现。

通常,这是通过mockImplementation()函数完成的,尽管由于我们实际上只是返回保存结果的data-我们可以改用Sugar函数。

mockResolvedValue()mockImplementation(() => Promise.resolve(value))相同。

放置好模拟后,让我们继续进行测试:

1
2
3
4
5
6
7
8
9
10
11
describe('test getPhotosByAlbumId', () => {
    afterEach(() => jest.resetAllMocks());

    it('fetches photos by album id', async () => {
        const photos = await getPhotosByAlbumId(3);
        expect(axios.request).toHaveBeenCalled();
        expect(axios.request).toHaveBeenCalledWith({ method: 'get', url: '/3/photos?_limit=3' });
        expect(photos.length).toEqual(3);
        expect(photos[0].albumId).toEqual(3)
    });
});

在每个测试用例之后,我们确保调用jest.resetAllMocks()函数以重置所有模拟的状态。

在我们的测试案例中,我们调用ID为3getPhotosByAlbumId()函数。 然后我们做出断言。

第一个断言期望调用axios.request()方法,而第二个断言检查是否使用正确的参数调用了该方法。 我们还检查返回的数组的长度为3,并且数组的第一个对象的albumId3

让我们使用以下命令运行新测试:

1
npm test

我们应该得到以下结果:

1
2
3
4
5
6
7
8
9
PASS  ./index.spec.js
  test getPhotosByAlbumId
    ? fetches photos by album id (7ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.853s, estimated 1s
Ran all test suites.

有了这种新的熟悉程度和经验,让我们快速比较一下Jest和Sinon的测试经验,后者也经常用于模拟。

锡诺·穆克斯vs笑话

Sinon.js和Jest具有不同的处理模拟概念的方式。 以下是需要注意的一些主要区别:

  • 在Jest中,将模拟文件放在node_modules文件夹旁边的__mocks__文件夹中时,会在测试中自动模拟Node.js模块。例如,如果您有一个名为__mock__/fs.js的文件,则每次在测试中调用fs模块时,Jest都会自动使用该模拟。另一方面,对于Sinon.js,您必须在需要测试的每个测试中使用sinon.mock()方法手动模拟每个依赖项。

  • 在Jest中,我们使用jest.fn().mockImplementation()方法用存根响应替换模拟函数的实现。一个很好的例子可以在这里的Jest文档中找到。在Sinon.js中,我们使用mock.expects()方法进行处理。

  • Jest提供了许多方法来使用其模拟API,尤其是模块。您可以在此处查看所有这些信息。另一方面,Sinon.js的模拟方法更少,并且公开了一个更简单的API。

  • Sinon.js模拟程序作为Sinon.js库的一部分提供,该库可以插入并与其他测试框架(如Mocha)和断言库(如Chai)结合使用。另一方面,Jest模拟是Jest框架的一部分,Jest框架还附带了自己的assertions API。

  • 当您测试可能不需要像Jest这样的框架的全部功能的小型应用程序时,Sinn.js模拟通常是最有益的。 当您已经有测试设置并且需要向应用程序中的一些组件添加模拟时,它也很有用。

    但是,当使用具有许多依赖关系的大型应用程序时,充分利用Jest的模拟API及其框架将对确保一致的测试体验非常有利。

    结论

    在本文中,我们研究了如何使用Jest模拟使用axios进行的HTTP调用。 我们首先将应用程序设置为使用axios作为我们的HTTP请求库,然后设置Jest来帮助我们进行单元测试。 最后,我们回顾了Sinon.js和Jest模拟之间的一些差异以及何时可以最好地使用两者。

    要了解有关Jest模拟的更多信息以及如何将其用于更高级的用例,请在此处查看其文档。

    与往常一样,本教程中的代码可以在GitHub上找到。