前端自动化测试 之 Jest 测试框架应用

开发 前端
在实际业务应用中,我们建议对可复用的组件、工具函数、工具类等一些无副作用,可预知结果的代码来进行单元测试。在前期开发过程中的投入会大于没有单元测试的投入,因为要写一些测试用例,还要执行测试用例,优化代码等。但是在长久迭代中,这种方法会比没有进行单元测试的模块更加稳定。

前端自动化测试 —— Jest 测试框架应用

http://zoo.zhengcaiyun.cn/blog/article/jest

图片

什么是自动化测试

在软件测试中,自动化测试指的是使用独立于待测软件的其他软件来自动执行测试、比较实际结果与预期并生成测试报告这一过程。在测试流程已经确定后,测试自动化可以自动执行的一些重复但必要的测试工作。也可以完成手动测试几乎不可能完成的测试。对于持续交付和持续集成的开发方式而言,测试自动化是至关重要的。   ——来自 WiKi 百科

为什么要用前端自动化测试

随着前端项目的发展,其规模和功能日益增加。为了提高项目的稳定性和可靠性,除了需要测试工程师外,前端自动化测试也成为了不可或缺的一环。采用前端自动化测试可以有效地提高代码质量,降低出错的概率,从而使项目更加健壮和更易维护。

前端自动化分类和思想

单元测试

又称为模块测试 ,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。在前端中,一个函数、一个类、一个模块文件,都可以进行单元测试,测试时每个模块都是互不干扰的。

图片

集成测试

是在单元测试的基础上,测试再将所有的软件单元按照概要设计规格说明的要求组装成模块、子系统或系统的过程中各部分工作是否达到或实现相应技术指标及要求的活动。用户的开始操作到结束操作这一整个行为流程可以当作集成测试。

图片

TDD 测试驱动开发(Test Driven Development)

开发流程:

图片

TDD 是趋向于白盒测试,需要开发者对当前编写的模块思路足够清晰。

优势:

  1. 长期减少回归 bug。
  2. 代码质量更好,可维护性高。
  3. 测试覆盖率高(先写测试用例,再实现功能)。
  4. 错误测试代码不容易出现(测试在开发之前执行)。

BDD 行为驱动开发(Behavior Driven Development)

开发流程:

图片

BDD 趋向于黑盒测试,只关注用户的一整套行为流程下来是否会成功。

优势:

  1. 对于用户行为的整个流程把控程度较高,对于开发人员来说这样安全感高。

如何自己写非框架测试用例

不使用测试框架,我们该如何测试自己的模块呢?如果我们想要测试下面的代码,应该需要两个值,一个是 期望值 ,另一个是函数执行的 结果值 ,我们需要对比两个值来进行判断当前函数是否通过了测试用例。

// index.js
function ZcyZooTeam(str) {
  return 'Zcy' + str;
}

需要下面的 if / else 进行判断当前的期望值 value 和结果值 result 是否相等,如果相等说明我们的测试用例通过了。我们将这两段代码复制到浏览器中,下面的执行不会通过,并会抛出错误,只有我们将传入值改为 ZooTeam 才会成功执行。

// no-jest.js
const result = ZcyZooTeam('Zero');
const value = 'ZooTeam';
if(result !== value) {
  throw Error(`ZcyZooTeam 结果应为${value}, 但实际结果为${result}`);
}

图片

是否能简化?

如果我们有多个函数需要测试,你应该不想写许多个 if / else 代码块吧?所以我们要将上面的代码块进行优化成一个函数。

// no-jest.js
function expect(result) {
  return {
    // 用于判断是否为期望值
    toBe(value) {
      if(result !== value) {
        throw Error(`结果应为${value}, 但实际结果为${result}`);
      }
      console.log('测试通过!');
    }
  }
}

// 执行测试
expect(ZcyZooTeam('Zero')).toBe('ZcyZooTeam');

经过上面的封装,我们就可以只写一行代码进行测试了!

如何能清晰地看到我测的是哪个呢?

虽然上面的封装只需要书写一行代码就可以测试了,但是我们不知道执行结果和测试用例之间的对应关系,我们需要输出的文字来告诉我们当前是哪个测试用例执行了。

// no-jest.js
// 再封装如下方法
function test(msg, fn) {
  try {
    fn();
    console.log(msg + '测试通过!');
  } catch (error) {
    console.log(msg + '测试未通过!' + error);
  }
}

test('测试ZcyZooTeam', () => {
  expect(ZcyZooTeam('Zero')).toBe('ZcyZooTeam')
})

成功和失败都会进行提示,这样我们就可以知道当前是哪个测试用例成功/失败了。

Jest 的书写方式也是同上,如果上面的一整套代码了解了的话,你已经可以写 Jest 的测试脚本了,下面将进入 Jest 的配置。

如何使用 Jest 测试框架进行自动化测试?

主流的前端自动化测试框架

Jasmine

Jasmine 优点:易于学习和使用,支持异步测试,可以在浏览器和 Node.js 环境中运行,可以生成易于阅读的测试报告,可以与其他库和框架集成。

MOCHA

MOCHA 优点:支持异步测试和 Promise ,可以在浏览器和 Node.js 环境中运行,可以与其他库和框架集成,可以生成易于阅读的测试报告,可以使用各种插件和扩展来增强其功能。

Jest

Jest 是针对模块进行测试,单元测试对单个模块进行测试,集成测试对多个模块进行测试。

Jest 优点:速度快(单模块测试时,执行过的模块不会重复执行),API简单,易配置,隔离性好(执行环境相对隔离,每个文件单独隔离互不干扰),监控模式(更灵活的运行各种测试用例),适配编辑器多,Snapshot(快照),多项目运行(后台前台测试用例并行测试),生成可视化覆盖率简单,Mock 丰富。

准备工作 —— Jest 的配置

npm i jest --save-D

// 初始化 jest 的配置文件
npx jest --init

// 你将在那个环境进行测试,回车即可选择
// 第一个是 node 环境、第二个是浏览器环境
? Choose the test environment that will be used for testing › - Use arrow-keys. Return to submit.
    node
❯   jsdom (browser-like)

// 是否需要 jest 生成测试覆盖率报告
? Do you want Jest to add coverage reports? › (y/N)

// 是否需要在测试结束后清除模拟调用
? Automatically clear mock calls and instances between every test? › (y/N)

// 创建 jest.config.js 文件
📝  Configuration file created at /Users/zcy1/Desktop/demo/auto-test-jest-demo/jest.config.js

以上方法执行结束后,会生成一个 jest.config.js 文件,里面包含了 Jest 的配置项,每个配置项都会带有描述,在初始化的两个配置也会体现在配置文件中。

使用 babel 转换来使用 ES6 形式的导入和导出

// .babelrc
// 如果想用 es6 的形式导出,需要使用 babel 插件进行转换
// @babel/core  @babel/preset-env

// 创建 .babelrc 文件
// 为了在 node 环境下使用 es6 的导出,需要使用 babel 进行转换
{
  // 设置插件集合
  "presets": [
    // 使用当前插件,可以进行转换
    // 数组的第二项为插件的配置项
    [
      "@babel/preset-env", {
        // 根据 node 的版本号来结合插件对代码进行转换
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

配置好后需要将 package.json 中的 test 命令的 value 改为 jest --watchAll ,代表监听所有有修改的测试文件,然后控制台执行 npm run test 就可以执行测试用例了。

Jest 启动时会进行如下流程

  1. npm run test
  2. jest (babel-jest) 检测当前环境是否安装了 babel
  3. 如果安装了则会去 babelrc 中取配置
  4. 取到后执行代码转换
  5. 最后再执行转化过的测试用例代码

如何生成一个测试用例覆盖率报告?

经过上面的 Jest 配置,我们就可以通过下面的 npx 命令来生成测试覆盖率报告了。

npx jest --coverage

会生成一个名为 coverage 的文件夹,打开里面的 html 就可以看到你的覆盖率,其中 Statements 是语句覆盖率(每个语句是否执行),Branches 是分支覆盖率(每个 if 块是否执行),Functions是函数覆盖率(每个函数是否执行),Lines 是行覆盖率(每行是否执行),通过修改 coverageDirectory 的值可以改变测试覆盖率生成文件夹的名字。

Jest 基础匹配器

上面我们说过了,Jest 的用法和我们封装的那几个函数是一样的,都是执行 test 函数并向函数中传递参数,第一个参数是你当前测试用例的描述,第二个参数是需要执行的匹配规则。

匹配器

toBe

toBe 匹配器,期待是否与匹配器中的值相等 相当于 object.is ===

// jest.test.js
test("测试", () => {
  expect(1).toBe(1);  // 通过
  const a = { name: 'Zero' };
  // 因为 a 的引用地址,和 toBe 中对象的引用地址不一致,会导致测试不通过,需要使用其他的匹配器
  expect(a).toBe({ name: 'Zero' });  // 失败
});
toEqual

toEqual 匹配器,只会匹配对象中的内容是否相等。

// jest.test.js
test('测试对象相等', () => {
  const a = { name: 'Zero' };
  expect(a).toEqual({ name: 'Zero' });  // 断言
})
toBeNull

toBeNull 匹配器,可以判断变量是否为 null ,只能匹配 null。

// jest.test.js
test('测试是否为null', () => {
  const a = null;
  expect(a).toBeNull();
})
toBeUndefined

toBeUndefined 匹配器,可以判断变量是否为 undefined ,只能匹配 undefined。

// jest.test.js
test('测试是否为undefined', () => {
  const a = undefined;
  expect(a).toBeUndefined();
})
toBeDefined

toBeDefined 匹配器,希望被测试的值是定义好的。

// jest.test.js
test('测试变量是否定义过', () => {
  const a = '';
  expect(a).toBeDefined();
})
toBeTruthy

toBeTruthy 匹配器,可以判断变量是否为真值,会对非 bool 值进行转换。

// jest.test.js
test('测试变量真值', () => {
  const a = '123';
  expect(a).toBeTruthy();
})
toBeFalsy

toBeFalsy 匹配器,可以判断变量是否为假值,会对非 bool 值进行转换。

// jest.test.js
test('测试变量假值', () => {
  const a = '';
  expect(a).toBeFalsy();
})
not修饰符

not 匹配器,可以将匹配后的结果进行取反。

// jest.test.js
test('测试变量不是假值', () => {
  const a = '1';
  expect(a).not.toBeFalsy();
})
toBeGreaterThan

toBeGreaterThan 匹配器,期望值是否大于匹配器的参数。

// jest.test.js
test('是否大于 a 的数字', () => {
  const a = 123;
  expect(a).toBeGreaterThan(1);
})
toBeLessThan

toBeLessThan 匹配器,期望值是否小于匹配器的参数。

// jest.test.js
test('是否小于 a 的数字', () => {
  const a = 0;
  expect(a).toBeLessThan(1);
})
toBeGreaterThanOrEqual

toBeGreaterThanOrEqual 匹配器,期望值是否大于或等于匹配器的参数。

// jest.test.js
test('是否大于等于 a 的数字', () => {
  // toBeLessOrEqual 匹配器,与之相反
  const a = 123;
  expect(a).toBeGreaterThanOrEqual(1);
})
toBeCloseTo

js 中,浮点数值在相加时不准确,使用 toBeCloseTo 匹配器解决,趋近于 0.3。

// jest.test.js
test('是否大于等于 a 的数字', () => {
  const a1 = 0.1;
  const a2 = 0.2;
  expect(a1 + a2).toBeCloseTo(0.3);
})
toMatch

toMatch 匹配器,匹配当前字符串中是否含有这个值,支持正则。

// jest.test.js
test('是否包含 day ', () => {
  const a = 'happy every day';
  expect(a).toMatch('day');
})
toContain

toContain 匹配器,判断当前数组中是否包含这个元素,Set 也可以使用。

// jest.test.js
test('数组中是否包含 zoo 这个元素', () => {
  const a = ['zoo', 'ZooTeam', 'Zero'];
  expect(a).toContain('zoo');
})
toThrow

toThrow 匹配器,可以捕捉抛出的异常,参数为抛出的 error ,可以用来判断是否为某个异常。

// jest.test.js
const error = () => {
  throw new Error('error');
}
test('是否存在异常', () => {
  expect(error).toThrow();
})

以上就是 Jest 中比较基础的匹配器,可以结合 初始化 + 配置 + 基础匹配器 进行书写测试用例。

命令行操作

在运行 npm run test 命令的时候,控制台执行测试用例成功或失败后都会像下面的图片一样出现几行提示,让你按对应的键进行操作。

图片

上面几个命令行的意思如下:

1. f 只会跑测试未通过的用例,再次点击 f 会取消当前模式。

我们使用一个失败的测试用例做一下示范。

图片

图片

按下 f 后,Jest 只会执行刚才失败的测试用例。

图片

2. 只监听已改变的文件,如果存在多个测试文件,可以开启,会与当前 git 仓库中的提交进行比较,需要使用 git 来监听哪个文件修改了,也可以将 --watchAll 改为 --watch 只会运行修改的文件。

3. 根据测试用例文件的正则表达式,过滤需要执行的测试用例文件,No tests found, exiting with code 0 如果填写不对会进行提示,并不会跑任何测试用例。

4. 根据测试用例描述的正则表达式,过滤需要执行的测试用例。5. 退出测试用例监听。

异步测试

在正常的业务开发中,项目中不只有同步代码,还会有请求接口的异步代码,异步代码的测试与同步代码有稍许不同,我们来看一下。

编写一个接口请求

// getData.js
export const getData = (fn) => {
  axios.get('/getData').then((res) => {
    fn(res.data);
  })
}

对异步请求进行测试

// jest.test.js
// 异步调用回调函数需要添加 done 参数,是一个函数
test('getData 返回结果为 { success: true }', (done) => {
  // 此处代码无效,因为测试用例不会等待请求结束后的回调,测试用例执行完就直接结束了
  // getData1((data) => {
  //   expect(data).toEqual({
  //     success: true
  //   })
  // })
  
  getData1((data) => {
    expect(data).toEqual({
      success: true
    })
    // 需要在结束前调用 done 函数, Jest 会知道到 done 才会结束,才可以正确测试异步函数
    done();
  })
})

需要注意的是,如果传入了形参 done,但是没有使用,这个测试用例就会处于一直执行的状态,直到执行超时。

图片

还可以结合 promise 进行使用

// getData.js
export const getData2 = () => {
  return axios.get('http://www.dell-lee.com/react/api/demo.json')
}

// jest.test.js
test('getData 返回结果为 { success: true }', () => {
  // 使用 promise 时需要 return,在 then 中使用 done 也可以
  return getData2().then(res => {
    expect(res.data).toEqual({
      success: true
    })
  })
})

// 测试请求是否 404
test('getData 返回结果为 404', () => {
  // 由于不触发 catch 就不会走测试校验,所以会成功,我们需要做一下限制
  // 这行代码限制下面的代码中必须要执行一次 expect 方法,如果非 404 就不会走下面的 expect,则测试不会通过
  expect.assertions(1);
  // 使用 promise 时需要 return
  // 如果只想测试 404 这样写是有问题的,需要配合 assertions 使用
  return getData2().catch(err => {
    expect(err.toString().indexOf('404') > -1).toBe(true)
  })
})

// 另一种写法
test('getData 返回结果为 { success: true }', () => {
  // 会返回很多数据,其中包含 data 对象
  // getData2().then((res) => console.log(res))
  // {
  //   status: 200,
  //   statusText: 'OK',
  //   headers: {},
  //   ......
  //   data: { success: true }
  // }
  // resolves 方法会将接口返回的字段全部获取,再使用 toMatchObject 方法进行匹配大对象中是否存在 data 对象
  return expect(getData2()).resolves.toMatchObject({
    data: {
      success: true
    }
  })
})

// 还可以使用 async/await
test('getData 返回结果为 { success: true }', async () => {
  await expect(getData2()).resolves.toMatchObject({
    data: {
      success: true
    }
  })
})

钩子函数

钩子函数可以当作一个测试用例的生命周期来看待,有 beforeAll 、beforeEach 、afterEach 、afterAll 。

以下是一些关于钩子函数的概念和场景:

beforeAll:在所有测试用例执行前运行

beforeEach:在每个测试用例执行前执行一次

afterEach:在每个测试用例执行后执行一次

afterAll:在所有测试用例结束后运行

有时候,需要测试一个类中的多个方法,这些方法可能会反复操作同一个对象上的属性。如果使用同一个实例,就会相互干扰,导致测试用例无法通过。此时,需要使用不同的实例来进行测试。

Counter 类

// Counter.js
class Counter {
  constructor() {
    this.number = 0;
  }

  add() {
    this.number += 1;
  }

  minus() {
    this.number -= 1;
  }
}

export default Counter;

我们想要测试里面的 add 和 minus 方法是否正确,需要实例化一个对象进行测试。但是下面的测试用例使用的永远都是同一个实例,第二个测试用例永远都不会通过。因为执行了第一个测试用例,第二个测试用例的值只能是 0。

// jest.test.js
const count = new Counter();
// 使用下方两种测试方法会互相影响,先加一后减一,结果永远是 0
test('测试加法', () => {
  count.add();
  expect(count.number).toBe(1);
})

test('测试减法', () => {
  count.minus();
  expect(count.number).toBe(-1);
})

需要使用钩子函数,在每次执行测试用例的时候,都让他重新实例化一个对象

// jest.test.js
let count = null;
// 类似于生命周期
// 会在测试用例执行前运行
beforeAll(() => {
  console.log('beforeAll')
});

// 会在每个测试用例执行前执行一次,这样就会解决上面互相影响的问题
beforeEach(() => {
  console.log('beforeEach')
  count = new Counter();
});

// 会在每个测试用例执行后执行一次
afterEach(() => {
  console.log('afterEach')
});

// 会在所有测试用例结束后运行
afterAll(() => {
  console.log('afterAll');
});

test('测试加法', () => {
  console.log('add')
  count.add();
  expect(count.number).toBe(1);
})

test('测试减法', () => {
  console.log('minus')
  count.minus();
  expect(count.number).toBe(-1);
})

分组方法 discribe

// jest.test.js
let count = null;
// describe 方法,可以将测试用例进行分组,更加好维护同类型功能的测试用例
describe('count 测试', () => {
  beforeAll(() => {
    console.log('beforeAll')
  });
  beforeEach(() => {
    console.log('beforeEach')
    count = new Counter();
  });
  afterEach(() => {
    console.log('afterEach')
  });
  afterAll(() => {
    console.log('afterAll');
  });
  
  // 将 add 类型进行分组
  describe('测试 add 类型用例', () => {
    // 在 describe 方法中,钩子函数会按照层级嵌套进行执行,先执行外部,再执行内部,不同的 describe 互不干扰
    beforeEach(() => {
      console.log('beforeEach add');
    });
    test('测试加法', () => {
      console.log('add')
      count.add();
      expect(count.number).toBe(1);
    })
  })

  // 将 minus 类型进行分组
  describe('测试 minus 类型用例', () => {
    test('测试减法', () => {
      console.log('minus')
      count.minus();
      expect(count.number).toBe(-1);
    })
  })
})

加上 describe 方法的执行效果如下图:

图片

Mock

在日常开发中,当前端开发差不多后,后端接口可能还没有提供,这个时候我们就要用 Mock 数据。而 Jest 也有 Mock 方法,用于模拟一些 JavaScript 的函数等。

我们先来一个比较简单的 mock.fn

// mock.js
export const runFn = (fn) => {
  fn(123);
}

// mock.test.js
test('测试 runFn', () => {
  // 通过 jest 的 fn 方法创建一个模拟函数,如果不传参数会默认生成一个函数
  // 1. 通过 func.mock 获取想要的值
  // 2. 可以自定义返回值
  // 3. 改变内部函数的实现,模拟接口请求,不请求代码中的接口
  const func = jest.fn( () => 456 );

  // 还可以使用 mockReturnValueOnce 方法进行控制输出,两种方法都使用时会覆盖 fn 方法中的返回值,支持链式调用
  // 将 Once 去掉与 fn 方法一样,多次会返回相同的值
  func.mockReturnValueOnce('zoo')

  // 返回 this 方法 mockReturnThis
  func.mockReturnThis();

  // 还可以使用 mockImplementation 方法书写函数内部,可以在函数内部写逻辑,与 jest.fn 方法的参数一样,还可以填加 Once
  func.mockImplementation(() => {
    return '123';
  })

  // 执行被测函数
  runFn(func);
  runFn(func);
  
  // console.log(func.mock)
  // 因为被调用了两次,所以长度都是 2
  // {
  //   calls: [ [123], [123] ],  // 每次的调用情况,传递的参数是什么
  //   instances: [ undefined, undefined ],  // 每次调用的 this 指向,被调用了几次
  //   invocationCallOrder: [ 1, 2 ],  // 执行顺序,可能会传入同一个或多个方法中,需要记录一下顺序
  //   results: [  // mock 函数每次执行后的返回值
  //     { type: 'return', value: 456 },
  //     { type: 'return', value: 456 }
  //   ]
  // }

  // 通过 toBeCalled 判断函数是否被调用
  expect(func).toBeCalled();

  // 判断当前函数调用了几次 被调用了两次
  expect(func.mock.calls.length).toBe(2);

  // 判断参数是什么
  expect(func.mock.calls[0]).toEqual([123]);

  // 判断每次调用的时候参数是什么
  expect(func).toBeCalledWith(123);

  // 判断返回值
  expect(func.mock.results[0].value).toBe('zoo');
})

Mock 高阶用法

如果需要通过修改请求的方式进行测试,而不使用测试框架,我们可能需要修改请求的代码逻辑。但是,Jest 提供了一种高级的 Mock 方法。我们只需在项目根目录下创建一个名为 __mocks__ 的文件夹,然后在其中自定义文件内容并导出,就可以使用自己定义的 Mock 函数而不必修改请求代码逻辑。

图片

图片

书写测试用例文件,引入 __mocks__ 文件夹中的函数

// mocker.test.js
// 使用 mock 方法引用 __mocks__ 下创建的 mock.js
jest.mock("./mock");
// 执行完上面的方法,会直接寻找 __mocks__ 下的getData,而不是正常的请求文件
// 由于 mock 中没有 getCode 方法,最好只 mock 异步函数,同步函数直接测试即可
// 可以不必须创建 __mocks__ 文件夹
import {
  getData,
} from "./mock";

// 需要使用下面的 requireActual 方法来引用非 mock 文件夹下的 getCode
const { getData } = jest.requireActual("./mock");

// 高阶mock
// 此处直接使用 __mocks__ 目录下的 mock 文件中的函数
test("测试 getData", () => {
  return getData().then((data) => {
    expect(eval(data)).toEqual("123");
  });
});

Mock-timers

在特定的业务中,需要使用到定时器,测试的时候也是需要修改代码来测试不同时间,最主要的一点是,我们需要等时间才能看到我们的执行结果,Jest 也有关于定时器的 Mock 函数。

// mock.js
export const timer = (fn) => {
  setTimeout(() => {
    fn();
    setTimeout(() => {
      fn();
    }, 3000)
  }, 3000)
}
// mock-timers.test.js
import { timer } from './mock';

// 使用 useFakeTimers 方法告知 Jest 在下面的测试用例,如果用到了定时器异步函数的时候,都是用假的 timers 进行模拟
jest.useFakeTimers();

test('测试 timer', () => {
  const fn = jest.fn();
  timer(fn);

  // 使用 runAllTimers 方法,让定时器立即执行,和 useFakeTimers 配合使用
  jest.runAllTimers();

  // 如果代码中有多个定时器嵌套,只想测试最外层的定时器,则需要使用 runOnlyPendingTimers 方法
  // 这个方法会只执行当前在队列中的函数,可以多次调用
  jest.runOnlyPendingTimers();
  jest.runOnlyPendingTimers();

  // advanceTimersByTime 方法,可以快进时间
  // 因为 timer 中,三秒后只执行了第一层,如果是六秒,则会执行两次 fn
  jest.advanceTimersByTime(3000);
})

snapshot 快照

到这里我们已经可以测试一些代码了,但是我们要如何捕捉执行结果和当前做对比呢?这时候就要使用快照功能了。

// snapshot.js
export const config1 = () => {
  return {
    method: 'GET',
    url: '/api',
    time: new Date()
  }
}

export const config2 = () => {
  return {
    method: 'GET',
    url: '/api',
    time: new Date().getTime()
  }
}
// snapshot.test.js
import { config1, config2 } from "./snapshot";

test('测试 config1 返回值', () => {
  // 但如果每次函数修改的时候,当前测试用例也要不断地修改
  // expect(config()).toEqual({
  //   method: 'GET',
  //   url: '/api'
  // });

  // 需要使用快照匹配 toMatchSnapshot 方法
  // 此方法会生成一个 __snapshots__ 目录,下面的文件中,第一次执行中 config 生成的结果会存到快照文件中
  // 快照会根据 test 方法中的描述生成一个映射关系
  // 修改后的 config 的执行结果与快照中的结果不同时会报错,需要更新快照
  // 如果 config 中有的值是每次运行都会变化的,那么每次快照都不会与当前执行相同,除非执行后再更新快照
  // 需要将在 toMatchSnapshot 方法中传递一个参数,设置一下 time 为任意格式的 Date 类型
  expect(config1()).toMatchSnapshot({
    time: expect.any(Date)
  });
})

test('测试 config2 返回值', () => {
  expect(config2()).toMatchSnapshot({
    time: expect.any(Number)
  });
})

行内快照生成

// snapshot.test.js
// 需要安装 prettier
test("测试 config2 返回值", () => {
  // toMatchInlineSnapshot 方法,将执行快照放到行内中,会放到 toMatchInlineSnapshot 方法中
  expect(config2()).toMatchInlineSnapshot(
    {
      time: expect.any(Number)
    },
    `
    Object {
      "method": "GET",
      "time": Any<Number>,
      "url": "/api",
    }
  `
  );
});

对 dom 节点测试

Jest 内部自己模拟了一套 jsDom ,可以在 node 的环境下执行需要浏览器环境 dom 的测试用例。

// dom.js
import $ from 'jquery';
const addDiv = () => {
  // jQuery
  $('body').append('<div/>');
}

export default addDiv;

// dom.test.js
import addDiv from './dom';
import $ from 'jquery';

test('测试 addDiv', () => {
  addDiv();
  addDiv();
  console.log($('body').find('div').length);

  // 测试dom
  expect($('body').find('div').length).toBe(2);
  expect(document.getElementsByTagName('div').length).toBe(2);
})

VSCode 插件

Jest Snippets 用于快速生成 Jest 代码块的工具。

Jest 能够检测当前文件夹中的测试用例并自动运行测试,还支持可视化操作,更新、执行以及单个执行等功能,非常方便!

常用配置解读

module.exports = {
  // 检测从哪个目录开始,rootDir 代表根目录
  roots: ["<rootDir>/src"],

  // 代码测试覆盖率通过分析那些文件生成的,!代表不要分析
  collectCoverageFrom: [
    // src 下所有 js jsx ts tsx 后缀的文件
    "src/**/*.{js,jsx,ts,tsx}",

    // src 下所有 .d.ts 后缀的文件
    "!src/**/*.d.ts"
  ],

  // 运行测试之前,我们额外需要准备什么
  setupFiles: ["react-app-polyfill/jsdom"],

  // 当测试环境建立好后,需要做其他事情时可以引入对应的文件
  setupFilesAfterEnv: ["<rootDir>/src/setupTests.js"],

  // 哪些文件会被认为测试文件
  testMatch: [
    // src 下的所有 __tests__ 文件夹中的所有的 js jsx ts tsx 后缀的文件都会被认为是测试文件
    "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",

    // scr 下的所有一 .test/spec.js/jsx/ts/tsx 后缀的文件都会被认为是测试文件
    "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}",
  ],

  // 测试运行的环境,会模拟 dom
  testEnvironment: "jsdom",

  // 测试文件中引用一下后缀结尾的文件会使用对应的处理方式
  transform: {
    // 如果引用的是 js jsx mjs cjs ts tsx 后缀的文件会使用 /config/jest/babelTransform.js 文件进行处理
    "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",

    // 如果引用的是 css 后缀的文件,会使用 /config/jest/cssTransform.js 文件处理
    "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",

    // 不是以 js jsx mjs cjs ts tsx css json 这些为后缀的文件会使用 /config/jest/fileTransform.js 文件进行处理
    "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)":
      "<rootDir>/config/jest/fileTransform.js",
  },

  // 忽略 transform 配置转化的文件
  transformIgnorePatterns: [
    // node_modules 目录下的 js jsx mjs cjs ts tsx 后缀的文件都不需要转化
    "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",

    // .module.css/sass/scss 后缀的文件都不需要转化
    "^.+\\.module\\.(css|sass|scss)$",
  ],

  // 自动化测试时,应用的模块应该从哪里寻找,默认是在 node_modules
  modulePaths: [],

  // 模块名字使用哪种工具进行映射
  moduleNameMapper: {
    // 针对于 native 移动端
    // "^react-native$": "react-native-web",

    // 将 .module.css/sass/scss 模块使用 identity-obj-proxy 工具进行转化
    "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
  },

  // 引入模块时,进行自动查找模块类型,逐个匹配
  moduleFileExtensions: [
    "web.js",
    "js",
    "web.ts",
    "ts",
    "web.tsx",
    "tsx",
    "json",
    "web.jsx",
    "jsx",
    "node",
  ],

  // 监听插件
  watchPlugins: [
    "jest-watch-typeahead/filename",
    "jest-watch-typeahead/testname",
  ],

  // 重置 mock
  resetMocks: true,
};

小结

在实际业务应用中,我们建议对可复用的组件、工具函数、工具类等一些无副作用,可预知结果的代码来进行单元测试。在前期开发过程中的投入会大于没有单元测试的投入,因为要写一些测试用例,还要执行测试用例,优化代码等。但是在长久迭代中,这种方法会比没有进行单元测试的模块更加稳定。

代码地址

  1. 前置 demo :https://github.com/Jadony/Jest-demo
  2. Jest 简单配置:https://github.com/Jadony/jest-config
  3. Jest 匹配器:https://github.com/Jadony/jest-matchers
  4. 异步代码测试:https://github.com/Jadony/jest-async
  5. Jest 钩子函数:https://github.com/Jadony/jest-hook
  6. Jest 的 mock 函数:https://github.com/Jadony/jest-mock
  7. Jest 的快照:https://github.com/Jadony/jest-snapshot
  8. Jest 对 Dom 节点的测试:https://github.com/Jadony/jest-dom

参考文档

  • 《前端要学的测试课 从Jest入门到TDD/BDD双实战》(https://coding.imooc.com/class/chapter/372.html#Anchor)
责任编辑:武晓燕 来源: 政采云技术
相关推荐

2021-06-26 07:40:21

前端自动化测试Jest

2021-06-30 19:48:21

前端自动化测试Vue 应用

2021-06-25 10:57:30

前端自动化测试开发

2009-08-19 09:00:48

单元测试框架自动化测试

2017-12-24 21:00:10

自动化测试测试框架敏捷

2011-04-18 12:52:37

自动化测试功能测试软件测试

2016-09-26 16:42:19

JavaScript前端单元测试

2022-09-14 10:00:12

前端自动化测试

2019-04-18 09:00:00

Java自动化测试框架

2011-06-03 17:06:09

自动化测试

2020-09-14 07:00:00

测试自动化框架

2020-04-28 09:00:00

测试测试自动化

2023-10-12 07:40:54

Minium自动化框架

2023-11-08 13:18:00

JestJavaScript框架

2011-12-23 17:09:57

自动化测试

2012-12-24 22:54:31

2014-04-16 14:15:01

QCon2014

2010-09-08 15:25:09

自动化测试技术网站链接测试

2011-08-16 15:36:47

iPhone应用测试

2017-01-16 13:38:05

前端开发自动化
点赞
收藏

51CTO技术栈公众号