likes
comments
collection
share

花一个小时,迅速掌握Jest的全部知识点~

作者站长头像
站长
· 阅读数 54

大家好,我是小杜杜,我们知道常用的测试类型有:功能测试单元测试集成测试冒烟测试等,其中单元测试是我们必须要掌握的一种技术,所以我们今天一起来看看为什么需要单元测试,作为一名React开发者,需要掌握哪些相关的技能

如果你有耐心地看完本章,你将会学习到以下内容:

  • 什么是单元测试?我们为什么需要?
  • 如何做一个简单的单元测试?
  • Jest 有哪些语法,Jest 的报告包含哪些信息?
  • 如何与ts兼容,需要哪些配置?
  • ...

同时附上一张今天的知识图,还请各位小伙伴多多支持~

花一个小时,迅速掌握Jest的全部知识点~

单元测试

有些小伙伴看到要讲单元测试,可能会下意识的避而远之,因为单元测试相对而言比较麻烦,而且从一定的角度来说,也比较鸡肋,原因主要有以下几点:

1.对于大多数人而言,实际的工作中并不需要(需求都写不完,怎么可能还去写单元测试)

2.Jest的语法相当于一门新的语言,是有一定的学习成本

3.配置麻烦,虽然Jest的上手文档看似非常简单,但实际情况是:只要有一个配置的不合适,所有的测试都运行不了,这点是比较难受的

以上几点,可能是大家并不想碰单元测试的主要原因,但单元测试确实也是不可或缺的一种实际技能,像一些开源的产品,都会有最基本的单元测试,所以单元测试是我们进阶中不可缺少的一部分~

什么是单元测试?

单元测试:指的是以原件的单元为单位,对软件进行测试。

其中单元可以是一个函数,也可以是一个模块一个组件,基本特征就是只要输入不变,必定返回同样的输出。

如果一个软件越容易些单元测试,就表明它的模块化结构越好,给模块之间的耦合越弱。

我们都知道React是非常自由的,其组件化的思想,React Hooks的普及,都非常适合进行单元测试

单元测试带来了哪些好处?

首先我认为单元测试是一个非常具有挑战性的活,因为它是一个不是成功就是失败的活,期间可能会出来各种各样的问题,动手实现下,还是比较有益的,接下来看看单元测试可以给我们带来哪些好处:

保质保量

单元测试可以有效的防止我们减少bug率

虽然bug我们无法避免,但没有人想bug多,单元测试会让我们不得不去思考一些异常场景,这样无形之中,增强了代码的质量

描述代码

对现有的代码进行描述,也就是将用例作为记录。

举个例子,我们有时写一个比较大的组件时,并没有比较好的示例来说明每个参数的效果,确保各种情况下,代码都能正常运行,而单元测试可以诠释这段代码的意义,并且可以供其他开发者查看,增强代码的可读性

提升个人实力

抛开其他的不谈,单元测试可以很好的提升专业领域

首先单元测试可以算作一个单独领域,不同的框架需要不同的配置,如果依赖的组件发生变化,受影响的组件就有可能发生错误。

其次,不同的环境、不同的测试框架都会有一定的冲突(版本问题)

最后,可以模拟各种环境,不同环境的Mock能力,比如:React中有react-testing-libraryNext中有@nestjs/testing

可以看出单元测试是一个非常大的模块,也是非常值得去研究的领域,总体来说利大于弊,还请想进阶的伙伴耐心的看完,相信一定会让你受益良多~

Jest

JestFacebook开源的一个前端测试框架,主要用于ReactReact Native的单元测试,并且被集成在create-react-app中。

Jest的特点

  1. 易用性:基于Jasmine,提供断言库,支持多种测试风格
  2. 适应性:Jest是模块化可扩展可配置
  3. 沙箱快照:Jest内置了JSDOM,能够模拟浏览器环境,并且并行执行
  4. 快照测试:Jest能够对React组件树进行序列化,生成对应的字符串快照,通过比较字符串提供高性能的UI检测
  5. Mock系统:Jest实现了一个强大的Mock系统,并支持自动手动mock
  6. 支持异步代码测试:支持Promiseasync/await
  7. 自动生成静态分析结果:内置Istanbul,测试代码覆盖率,并生成对应的报告

第一个测试程序

初始化项目

mkdir jest-test
cd jest-test

// 初始化package.json文件
npm init -y

// 安装jest
npm i -D jest

初始化jest

// 执行
npx jest --init

此时会弹出一堆问题供你选择,当你选择完后会生成一个jest.config.js的文件,这个文件就是关于Jest配置的文件。

花一个小时,迅速掌握Jest的全部知识点~

再来看看jest.config.js文件:

module.exports = {
  clearMocks: true,
  collectCoverage: true,
  coverageDirectory: "coverage",
  coverageProvider: "v8",
};

示例

我们在目录下创建src/sum.js文件 和src/sum.test.js文件

// src/sum.js
const sum = (a, b) => {
  return a + b;
}

module.exports = sum;

// src/sum.test.js
const sum = require('./sum');

describe('sum', () => {
  it('求和:1 + 2 = 3', () => {
    expect(sum(1, 2)).toEqual(3);
  });
})

执行

此时,我们就可以执行命令

// npx jest
npm run test

就可以看到一下信息 花一个小时,迅速掌握Jest的全部知识点~

单独执行

当我们的项目比较大的时候,只需要测试当前的文件即可

//npx jest
npm run test 文件路径

这里的文件路径是相对路径,会自动匹配格式相同的文件,如:

花一个小时,迅速掌握Jest的全部知识点~

Jest 配置讲解

我们已经完成了一个简单的jest测试,接下来我们讲解下各个文件的配置模块,方便我们之后的进行

配置文件

我们通过npx jest --init生成一个jest.config.js的文件,这个文件就是有关jest的配置文件

此外,你可以通过npx jest --help查看所有Jest Cli选项

由于配置的字段非常多,这里介绍一些常用的配置,具体的字段可以查看:配置 Jest

三种方式

第一种:我们可以将配置文件写入jest.config.js,返回一个对象或是一个函数,如:

// 对象
import type {Config} from 'jest';

const config: Config = {
    verbose: true,
    ...
};

export default config;

// 函数
import type {Config} from 'jest';

export default async (): Promise<Config> => {
    return {
        verbose: true,
        ...
    };
};

第二种:可以像package.json配置一个对应的json文件,命名为:jest.config.json, 如:

{
    "bail": 1,
    "verbose": true,
    ...
}

第三种:直接写在package.json中,写在jest中,如:

{
  ...
  "jest": {
    "verbose": true
  }
  ...
}

具体字段

基本字段
  1. clearMocks:类型:boolean,默认:false,在每次测试前自动清除模拟的上下文
  2. collectCoverage:类型:boolean,默认:false,是否开启覆盖率
  3. coverageDirectory:类型:string,默认:undefined,生成的覆盖率文件的文件位置
  4. coverageProvider:类型:string,babel(默认)/v8,指示应使用哪个提供程序来检测代码以进行覆盖
  5. maxConcurrency:类型:number,默认:5,限制使用时允许同时运行的测试数量的数字。
  6. preset:类型:string,默认:undefined,预设字段,应该指向哪个文件,可以是文件包或路径,比如安装ts-jest转译
  7. transform:类型 object\<string, pathToTransformer | [pathToTransformer, object]>,默认:{"^.+\\.[jt]sx?$": "babel-jest"},转换器,可以使用正则,用以匹配路径
  8. transformIgnorePatterns:类型:array<string>,默认:["/node_modules/", "\\.pnp\\.[^\\\/]+$"],在转换之前与所有源文件路径匹配的正则表达式模式字符串数组。如果文件路径与任何模式匹配,则不会对其进行转换。
  9. displayName:类型:string | object,默认:undefined,通过这个参数可以直接告诉测试属于哪个项目
  10. cacheDirectory:类型:string,默认:/tmp/<path>,用来储存依赖信息缓存的目录。
  11. testTimeout:类型:number,默认:5000,测试默认的超时时间
  12. testEnvironment:类型:string,默认:node,用于模拟测试的测试环境,如果我们用到浏览器的环境(如:document),可以用jsdom代替
  13. testMatch:类型:array<string>,用于匹配对应文件下的文件
  14. testPathIgnorePatterns:类型:array<string>,默认:["/node_modules/"],在检测的时候跳过一些文件

...

collectCoverageFrom

类型:array,默认:undefined,通过这个参数可以设置哪些文件收集配置信息,如:

import type {Config} from 'jest';

const config: Config = {
    collectCoverageFrom: [
        '**/*.{ts,tsx}',
        '!**/node_modules/**',
        '!**/vendor/**',
    ],
};

export default config;

这个配置将会收集rootDir下的ts, tsx的所有文件

并不会匹配**/node_modules/** 或 **/vendor/** 的文件

projects

类型:array<string | ProjectConfig>,默认:undefined,我们可以通过这个参数来配置我们的项目,它会提供一组路径或 glob 模式时,Jest 将同时在所有指定项目中运行测试。

比如:

import type {Config} from 'jest';

const config: Config = {
    projects: ['<rootDir>', '<rootDir>/examples/*'],
};

export default config;

这个配置可以在根目录以及示例目录中的每个文件夹中运行 Jest

再举一个例子:

const config: Config = {
    projects: [{
        testEnvironment: 'jsdom',
        displayName: 'react-jest',
        testMatch:[`<rootDir>/react-jest/src/**/*.test.ts?(x)`],
        testPathIgnorePatterns: ['/node_modules/'],
        cacheDirectory: `./node_modules/.cache/jest`,
        testTimeout: 30000,
        ...
    }],
};

像上面这个,我们知道jest的默认环境为node,但我们在react-jest的环境希望是浏览器的环境,就可以单独设置react-jest下的文件为jsdom,匹配testtstsx文件,去除一些文件(如node_modules)和一些其他的配置

全局设定

我们通过sum.test.js文件,发现了describeexpect,但我们并没有引入对应的函数,却能正常的使用,这是为什么呢?

实际上Jest会将这些方法和对象注入到测试文件的全局环境里,所以我们在使用的时候并不需要通过importrequire

当然,如果你一定要引用,可以这样引用:

import {describe, expect, test} from '@jest/globals

我们主要讲一下6个主要的方法,更多的方法还请参考:Jest-全局设定

describe

describe: 描述块,将一组功能相关的测试用例组合在一块

用法:describe(name, fn)

  • name:string,描述的话语
  • fn:() => void,将所有的代码写到这个函数里即可

it

it: 别名test, 用来存放测试用例,可以说有几个it就会有几个测试用例

用法:it(name, fn, timeout)test(name, fn, timeout)

  • name:string,测试名称
  • fn:() => void,包含测试期望的函数
  • fn:number,默认:5s,可选,测试的超时时间

afterAll 和 beforeAll

afterAll: 所有的测试用例执行完执行的方法,如果传入的回调函数返回值是 promise 或者 generatorJest 会等待 promise resolve 再继续执行。

beforeAll: 与afterAll相反, 所有的测试用例执行之执行的方法

用法:afterAll(fn, timeout)

  • fn:() => void,执行的函数
  • timeout:number,默认:5s,可选,测试的超时时间

afterEach 和 beforeEach

afterEach: 也 afterAll相比,afterEach可以在每个测试完成都运行一遍

beforeEachbeforeEach可以在每个测试完成之都运行一遍

用法:afterEach(fn, timeout)

  • fn:() => void,执行的函数
  • timeout:number,默认:5s,可选,测试的超时时间

示例

为了更好的理解,我们简单的写个示例,如:

const sum = require('./sum');

beforeAll(() => {
  console.log('全局之前');
});

afterAll(() => {
  console.log('全局之后');
});

beforeEach(() =>{
  console.log('全局之前,每个都会执行');
});

afterEach(() => {
  console.log('全局之后,每个都会执行');
});

describe('求和', () => {
  beforeAll(() => {
    console.log('求和:全局之前');
  });
  
  afterAll(() => {
    console.log('求和:全局之后');
  });
  
  beforeEach(() => {
    console.log('求和:全局之前,每个都会执行');
  });
  
  afterEach(() => {
    console.log('求和:全局之后,每个都会执行');
  });

  it('求和:1 + 2 = 3', () => {
    expect(sum(1, 2)).toEqual(3);
  });

  it('求和:2 + 5 = 7', () => {
    expect(sum(2, 5)).toEqual(7);
  });
})

执行结果:

花一个小时,迅速掌握Jest的全部知识点~

断言

断言:Expect,我们在测试的时候常常需要判断,是否满足一个结果,比如像上面的expect(sum(1, 2)).toEqual(3)就是为了验证,执行完sum函数后,结果是否等于3

接下来我们来看看一些常见的断言,详情:Expect 断言

基本断言

  1. expect(value):要测试一个值进行断言的时候,首先使用expect对值进行包裹
  2. not: 用于测试相反的结果,也就是不等于
  3. toMatch(regexpOrString):用来检查字符串是否匹配,可以是正则表达式或者字符串
  4. toContain(item):用来判断item是否在一个数组中,也可以用于字符串的判断
  5. toBeNull(value):只匹配null
  6. toBeUndefined(value):只匹配undefined
  7. toBeGreaterThan(number): 大于
  8. toBeGreaterThanOrEqual(number):大于等于
  9. toBeLessThan(number):小于
  10. toBeLessThanOrEqual(number):小于等于
  11. toBeInstanceOf(class):判断是不是class的实例
  12. anything(value):匹配除了null和undefined以外的所有值
  13. toHaveBeenCalled():用来判断mock function是否被调用过
  14. toHaveBeenCalledTimes(number):用来判断mock function被调用的次数
  15. assertions(number):验证在一个测试用例中有number个断言被调用

toBeUndefined 和 toBeDefined

toBeUndefined():用于检查变量是否未定义,也就是说,只会匹配undefined,比如说上面的sum返回的结果明显不是undefined,如果使用,则会报错

toBeDefined():与toBeUndefined相反,必须要匹配的有值的情况

如:

describe('求和', () => {
  it('求和:1 + 2 = 3', () => {
    expect(sum(1, 2)).toBeUndefined(); //error
  });

  it('求和:1 + 2 = 3', () => {
    expect(sum(1, 2)).toBeDefined(); // ok
  });
})

toBe 和 toEqual

toBe(value): 使用Object.is来进行比较,严格的比较,需要注意的是,如果进行浮点数的比较,要使用toBeCloseTo,可以理解为 ===

toEqual(value): 用于对象的深度,它一般比较的是对象的值,而并非对象本身

那么 toBetoEqual有什么区别呢?接下来一起看看:

对于基本类型:字符串、数字、布尔等, toBetoEqual并没有任何区别,而区别点主要是在对象上,如:

const data1 = {
  name: '小杜杜',
  age: 7,
};
const data2 = {
  name: '小杜杜',
  age: 7,
};

可以看到 data1data2 都有相等的nameage,那么data1 === data2 吗?

很明显,它俩并不相等,原因是:它俩的指向不同,所以当我们使用toBe的时候就会报错,这是个正常现象

但我们现在并不是比较地址,而是比较data1data2的每一项是否相等,这时就会用到toEqual,换言之,toEqual会忽略两个对象的指向,只是会比较数值,固:

  it('toEqual', () => {
    expect(data1).toEqual(data2); //ok
  });
  
  it('toBe', () => {
    expect(data1).toBe(data2); // error
  });

toBeTruthy 和 toBeFalsy

当我们不在乎返回的是什么的时候,只在乎返回的值是否是真的时候,或是为假的时候可以使用 toBeTruthy(为真) 和 toBeFalsy(为假)

我们先看下面这个例子:

const data = (count) => {
  if( typeof count === 'number'){
    return count
  }
  return undefined
}

describe('测试 toBeTruthy 和 toBeFalsy', () => {
  it('toBeTruthy', () => {
    expect(data(9)).toBeTruthy(); // ok
  });
  it('toBeFalsy', () => {
    expect(data('小杜杜')).toBeFalsy(); // ok
  });
  it('toBeTruthy === 0', () => {
    expect(data(0)).toBeTruthy(); // error
  });
});

data函数为数字的时候,就会返回数字,为其他的类型将会统一返回undefined

那么对应的,如果是数字,则为真值,toBeTruthy() 会校验通过

如果不是数字,toBeFalsy() 会检验通过,toBeTruthy() 不会通过

但当数字为0,返回为0时,toBeTruthy()也不会通过,这点要注意一下,这里的假值是指:false0null""undefinedNaN六种类型

测试异步代码

先看看这段异步代码:

const fetchData = (flag = true) => {
  return new Promise((resolve, reject) => {
    if(flag){
      resolve('小杜杜')
    }else{
      reject('error 你应该选择rejects')
    }
  })
}

我们该如何测试这段代码呢?这里提供三种方式

then方式
  it('then方式', () => {
    return fetchData().then(data => {
      expect(data).toBe('小杜杜');
    })
  });
async await 方式
  it('resolves 方式', () => {
    expect(fetchData()).resolves.toBe('小杜杜');
  });
resolves 和 rejects

resolvesrejects:主要用作promiseresolve/reject包裹的值,并且支持链式调用

如:

  it('resolves 方式', () => {
    expect(fetchData()).resolves.toBe('小杜杜');
  });

  it('rejects 方式', () => {
    expect(fetchData(false)).rejects.toMatch('error');
  });

覆盖率报告

终端上的结果

接下来我们看看 上述例子中,运行 npx jest 的结果 花一个小时,迅速掌握Jest的全部知识点~

  • %stmts:是语句覆盖率(statement coverage),是不是每个语句都执行了
  • %Branch:是分支覆盖率(branch coverage),是不是每个if代码块都执行了
  • %Funcs:是函数覆盖率(function coverage,是不是每个函数都调用了
  • %Lines:是行覆盖率(line coverage,是不是每一行都执行了

报告文件

还记得最开始的coverageDirectory: "coverage"吗?

实际上,终端展示的信息只是一部分,而生成的coverage文件会生成很多覆盖率文件,包括:XMLJSONHTML等,当然这些文件的内容是相通的,只是为了不同工具的提取,来看看文件目录:

花一个小时,迅速掌握Jest的全部知识点~

然后我们打开 sum.js.html来看看结果

花一个小时,迅速掌握Jest的全部知识点~

搭建环境

由于Ts的迅速崛起,我们在项目中往往会使用Ts,那么该如何进行配置呢? (有对TS不了解的小伙伴,可以看看这篇文章:一篇让你完全够用TS的指南

首先,请大家牢记:Jest,本身不支持转译,所以我们需要通过其他的转译器来帮助我们

将第一个测试程序改造为ts

在这里我们用ts-jest进行一下简单的改造,因为ts-jest是官方推荐的

安装ts

执行命令:

npm i -D typescript

安装完后要初始化ts的配置

npx tsc --init

此时会生成一个tsconfig.json的文件

花一个小时,迅速掌握Jest的全部知识点~

安装ts-jest

npm i -D @types/jest //jest 类型
npm i -D ts-jest //ts-jest

这里需要注意下:有关jest的版本尽量保持一致(大版本下),否则容易出现兼容性问题

配置

jest.config.js文件加入:

module.exports = {
  preset: 'ts-jest',
  ...
};

同时在tsconfig.json中加入

{
  "compilerOptions": {
    "types": ["node", "jest"],
    ...
  }
}

最后,我们在将sum.jssum.test.js改为ts文件即可

运行

花一个小时,迅速掌握Jest的全部知识点~

creat-react-app 融合jest

上面我们说过jest被集成在create-react-app,接下来,我们看看怎么被集成

创建程序

我们直接使用create-react-app创建一个ts的程序,执行命令

npx create-react-app react-jest-test(文件名) --template typescript

cd react-jest-test
npm i -D react-test-renderer //添加快照

然后把sum.tssum.test.ts复制到src下面

模块化

执行下看看:

花一个小时,迅速掌握Jest的全部知识点~

这个问题,出现的原因是不支持import 这种导入

解决:引入babel-plugin-transform-es2015-modules-commonjs

npm install --save-dev babel-plugin-transform-es2015-modules-commonjs

然后在根目录下创建.babelrc文件,加入:

{
  "plugins": ["transform-es2015-modules-commonjs"]
}

解决ts问题

然后我们再运行一遍,看看:

花一个小时,迅速掌握Jest的全部知识点~

这个问题还是上述说的转译问题,我们需要加入babelts的依赖

npm i -D babel-jest @babel/core @babel/preset-env
npm i -D @babel/preset-typescript

然后在跟目录下创建babel.config.js文件,加入:

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current'
        }
      }
    ],
    '@babel/preset-typescript'
  ]
};

特别注意,当你设置了babel.config.js后又可能还是运行不起来,这是因为babel的设置有关,需要将扩展名跟改为cjs,也就是babel.config.cjs

此时就配置成功了 花一个小时,迅速掌握Jest的全部知识点~

End

参考

总结

通过上面的介绍,我们发现Jest实际上是可以单独作为一个模块拿出来的,我们通过一个简单的示例,全面介绍了有关Jest的语法,方便我们后续的学习

当然 Jest 远远不止这些,希望各位小伙伴耐心的看完,并且尝试一下,在这里还是希望小伙伴能完全熟悉这些概念,方便后续的学习,下篇会将一个关于hooks的单元测试(一文玩转React Hooks的单元测试),还希望各个小伙伴多多支持~

如果你喜欢,请不要吝惜你的赞,有更好的建议,欢迎评论区讨论,一起走向进阶之路~

转载自:https://juejin.cn/post/7145269660635070495
评论
请登录