前端自动化测试之 Jest 入门(一)

1,161 阅读8分钟

TDD与单元测试

什么是TDD

TDD(Test Driven Development),测试驱动开发,是一种测试驱动开发过程的模式

简单的来说就是先编写测试代码,然后以使得所有测试代码都通过为目的,编写逻辑代码,

TDD 比较关注代码内部如何实现,关注事件是否触发?属性是否设置?data 数据是否被更新?比如:

describe("input 输入回车,向外触发事件,data 中的 inputValue 被赋值", () => {
  const wrapper = shallowMount(TodoList);
  const inputEle = wrapper.find("input").at(0);
  const inputContent = "用户输入内容";
  inputEle.setValue(inputContent);
  // expect:add 事件被 emit
  except(wrapper.emitted().add).toBeTruthy();
  // expect:data 中的 inputValue 被赋值为 inputContent
  except(wrapper.vm.inputValue).toBe(inputContent);
});

编写完此测试用例后,我们可以再去写实际的逻辑代码

单元测试

所谓单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证

通俗的讲,在前端,单元可以理解为一个独立的模块文件,单元测试就是对这样一个模块文件的测试

对于一个独立的模块(ES6模块),因为功能相对独立,所以我们可以首先编写测试代码,然后根据测试代码指导编写逻辑代码

所以提到 TDD 这里的测试一般是指单元测试

BDD与集成测试

什么是BDD

BDD(Behavior Driven Development),即行为驱动开发,是一种以用户行为来驱动开发过程的开发模式

先写完业务代码,然后站在用户的角度去测试功能,不关注代码实现过程,只是通过模拟用户操作测试功能,比如:

describe("TodoList 测试", () => {
  it(`
    1. 用户在 header 输入框输入内容
    2. 键盘回车
    3. 列表项增加一项,内容为用户输入内容
  `, () => {
    // 挂载 TodoList 组件
    const wrapper = mount(TodoList);
    // 模拟用户输入
    const inputEle = wrapper.find("input").at(0);
    const inputContent = "用户输入内容";
    inputEle.setValue(inputContent);
    // 模拟触发的事件
    inputEle.trigger("content");
    inputEle.trigger("keyup.enter");
    // expect:列表项增加对应内容
    const listItems = wrapper.find(".list-item");
    expect(listItems.length).toBe(1); // 增加 1 项
    expect(listItems.at(0).text()).toContain(inputContent); // 增加 1 项
  });
});

集成测试

集成测试(Integration Testing),是指对软件中的所有模块按照设计要求进行组装为完整系统后,进行检查和验证

通俗的讲,在前端,集成测试可以理解为对多个模块实现的一个交互完整的交互流程进行测试

对于多个模块(ES6模块)组成的系统,需要首先将交互行为完善,才能按照预期行为编写测试代码

所以提到 BDD 这里的测试一般是指集成测试

前端自动化测试

  • 通过提前书写的代码进行测试

假如我们定义了两个函数:

function add(a, b) {
  return a + b;
}

function minus(a, b) {
  return a - b;
}

如果我们手动去测试:

var result = add(1, 2);
var expected = 3;

if (result !== 3) {
  throw new Error(`1 + 2 应该等于 ${expected},但是结果却是${result}`);
}

var result = minus(2, 2);
var expected = 0;
if (result !== 0) {
  throw new Error(`2 - 2 应该等于 ${expected},但是结果却是${result}`);
}

封装简化上述测试代码:

function expect(result) {
  return {
    toBe(actual) {
      if (result !== actual) {
        throw new Error("预期值和实际值不相等");
      }
    }
  };
}

expect(add(1, 2)).toBe(3);
expect(minus(2, 2)).toBe(0);

上面如果多个测试实例都发生错误,无法定位:

function expect(result) {
  return {
    toBe(actual) {
      if (result !== actual) {
        throw new Error(`预期值和实际值不相等,预期${actual},结果却是${result}`);
      }
    }
  };
}

function test(desc, fn) {
  try {
    fn();
    console.log(`${desc}通过测试`);
  } catch (e) {
    console.log(`${desc}没有通过测试 ${e}`);
  }
}

test("测试加法 1 + 2", () => {
  expect(add(1, 2)).toBe(3);
});
test("测试减法 10 - 5", () => {
  expect(minus(10, 5)).toBe(0);
});

代码执行结果:

初识Jest

  • 速度快
  • API 简单
  • 易配置
  • 隔离性好
  • 监控模式
  • IDE 整合
  • Snapshot
  • 多项目并行
  • 覆盖率
  • Mock 丰富

使用npm init -y生成 package.json 文件

安装npm i jest@24.8.0 -D

我们需要使用 common.js 模块机制:

function add(a, b) {
  return a + b;
}

function minus(a, b) {
  return a - b;
}

function multi(a, b) {
  return a * b;
}

module.exports = {
  add,
  minus,
  multi
};

math.test.js

const math = require("./math.js");

const { add, minus, multi } = math;

test("测试加法 1 + 2", () => {
  expect(add(1, 2)).toBe(3);
});

test("测试减法 10 - 5", () => {
  expect(minus(10, 5)).toBe(5);
});

test("测试乘法 3 * 3", () => {
  expect(multi(3, 3)).toBe(9);
});

改变 package.json 文件后,运行 npm run test,寻找当前目录以.test结尾的文件去测试

"scripts": {
  "test": "jest --watchAll"
},
// jest --watch 则只会监听修改的文件

运行结果:

Jest的简单配置

  • npx jest --init 暴露出 jest 一些配置项

  • packge.json 里可添加  "coverage": "jest --coverage" 命令生成测试报告

  • npm i @babel/core@7.4.5 @babel/preset-env@7.4.5 -D 使得将 common.js 语法转换 esmoudle 语法

配置 .babelrc

{
    "presets": [
            [
                    "@babel/preset-env",
                    {
                            "targets": {
                                    "node": "current"
                            }
                    }
            ]
    ]
}

此时就可以使用 esmoudle 语法引入了

export function add(a, b) {
  return a + b;
}

export function minus(a, b) {
  return a - b;
}

export function multi(a, b) {
  return a * b;
}

math.test.js

import { add, minus, multi } from "./math";

test("测试加法 1 + 2", () => {
  expect(add(1, 2)).toBe(3);
});

test("测试减法 10 - 5", () => {
  expect(minus(10, 5)).toBe(5);
});

test("测试乘法 3 * 3", () => {
  expect(multi(3, 3)).toBe(9);
});

Jest的匹配器

更多具体的可查看 Expect 断言 · Jest (jestjs.io)

// 类似于Object.is
test("测试toBe匹配器", () => {
  const obj = { a: 1 };
  expect(obj).toBe({ a: 1 }); // 报错
});

// 匹配内容相等
test("测试toEqual匹配器", () => {
  const obj = { a: 1 };
  expect(obj).toEqual({ a: 1 }); // 通过
});

// 严格匹配null
test("测试toBeNull匹配器", () => {
  const a = null;
  expect(a).toBeNull(); // 通过
});

// 严格匹配undefined
test("测试toBeUndefined匹配器", () => {
  const a = undefined;
  expect(a).toBeUndefined(); // 通过
});

// 真假匹配器,匹配已定义
test("测试toBeDefined匹配器", () => {
  const a = null;
  expect(a).toBeDefined(); // 通过
});

// 真假匹配器,匹配JS转换布尔为真的内容
test("测试toBeTruthy匹配器", () => {
  const a = 1;
  expect(a).toBeTruthy(); // 通过
});

// 真假匹配器,匹配JS转换布尔为假的内容
test("测试toBeFalsy匹配器", () => {
  const a = "";
  expect(a).toBeFalsy(); // 通过
});

// 取反
test("测试not匹配器", () => {
  const a = true;
  expect(a).not.toBeFalsy(); // 通过
  // 相当于
  // expect(a).toBeTruthy(); // 通过
});

// 数字相关匹配器 期望数大于某数
test("测试toBeGreaterThan匹配器", () => {
  const count = 10;
  expect(count).toBeGreaterThan(9); // 通过
});

// 数字相关匹配器 期望数大于等于某数
test("测试toBeGreaterThanOrEqual匹配器", () => {
  const count = 10;
  expect(count).toBeGreaterThanOrEqual(10); // 通过
});

// 数字相关匹配器 期望数小于某数
test("测试toBeLessThan匹配器", () => {
  const count = 10;
  expect(count).toBeLessThan(11); // 通过
});

// 数字相关匹配器 期望数小于等于某数
test("测试toBeLessThanOrEqual匹配器", () => {
  const count = 10;
  expect(count).toBeLessThanOrEqual(10); // 通过
});

// 数字相关匹配器 期望数小于等于某数
test("测试toBeCloseTo匹配器", () => {
  const firstNumber = 0.1;
  const secondNumber = 0.2;
  expect(firstNumber + secondNumber).toBeCloseTo(0.3); // 通过,因为二者相加接近0.3,如果使用toEqual会报错,因为JS小数的精度问题
});

// 字符串匹配器 包含某数
test("测试toMatch匹配器", () => {
  const str = "hello";
  expect(str).toMatch("el"); // 通过 toMatch还可接收正则表达式
});

// 数组匹配器 包含某值
test("测试toContain匹配器", () => {
  const arr = ["a", "b"];
  expect(arr).toContain("a"); // 通过
});

// set匹配器 包含某值
test("测试toContain匹配器", () => {
  const arr = ["a", "b"];
  const set = new Set(arr);
  expect(set).toContain("a"); // 通过
});

// 异常处理器
const throwNewError = () => {
  throw new Error("this is new error");
};
test("测试toThrow匹配器", () => {
  expect(throwNewError).toThrow(); // 通过
  expect(throwNewError).toThrow("this is new error"); // 通过,同时匹配错误内容,也可接受正则表达式
});

当我们想忽略掉单个文件中的其他测试用例,只针对一个测试用例做调试的时候,可以加上 .only,但这不会忽略其他测试文件的测试用例

test.only("test only", () => {
  // ...
})

Jest命令行工具的使用

  • mac 用户 vscode 里按住command + shift + p,windows 用户按住ctrl + shift + p
  • 写入 install code command
  • 这样就可以在命令行 code ./ 直接以当前文件夹打开 vscode

  • a:全部重新执行用例
  • f:只执行失败的用例
  • o:只执行更改的文件的用例(需要和 git 结合使用,会比较现有文件和 commit 的文件的差异,只测试差异文件)
  • p:只测试指定的文件的用例
  • t:只测试指定的用例
  • q:退出监视代码
  • enter:重新运行测试

测试异步代码

对下面三个异步请求进行测试:

fetchData.js

import axios from "axios";

export function getData1(fn) {
  axios.get("http://www.dell-lee.com/react/api/demo.json").then(res => {
    fn(res.data);
  });
}

export function getData2() {
  return axios.get("http://www.dell-lee.com/react/api/demo.json");
}

export function get404() {
  return axios.get("http://www.dell-lee.com/react/api/404.json");
}

对于异步代码测试,时机很重要,必须保证我们的测试用例在异步代码走完之后才结束。有以下几种办法

  1. done:控制测试用例结束的时机
  2. 如果函数执行的返回值是 Promise,将这个 Promise return 出去
  3. async + await
import { getData1, getData2, get404 } from "./math";

test("getData1方法", done => {
  getData1(data => {
    expect(data).toEqual({
      success: true
    });
    done();
  });
});

test("return getData2方法", () => {
  return getData2().then(res => {
    expect(res.data).toEqual({
      success: true
    });
  });
});

test("await getData2方法", async () => {
  const { data } = await getData2();
  expect(data).toEqual({
    success: true
  });
});

test("get404方法", done => {
  // 进行断言:下面一定会执行一个 expect
  expect.assertions(1);
  // 正常获取数据不会走catch语法
  get404().catch(err => {
    expect(err.toString()).toMatch("404");
    done();
  });
});

测试 Promise 成功或失败除了在 then 里面判断,也可以使用:

test("resolves getData2方法",async () => {
  // return 可替换成 await
  return expect(getData2()).resolves.toMatchObject({
    data: {
      success: true
    }
  });
});

test("rejects get404方法", async () => {
  // return 可替换成 await
  return expect(get404()).rejects.toThrow();
});

test("try catch get404方法", async () => {
  // 进行断言:下面一定会执行一个 expect
  expect.assertions(1);
  try {
    await get404();
  } catch (e) {
    expect(e.toString()).toEqual("Error: Request failed with status code 404");
  }
});

Jest钩子函数

  • beforeAll 所有用例执行之前
  • beforeEach 每个用例执行之前
  • afterEach 每个用例执行之后
  • afterAll 所有用例执行之后

这四个钩子函数都会在适当的时机自动执行

测试前后如果做一些处理,尽量写在这些钩子函数之中,它能保证一定的执行顺序

beforeAll(() => {
  // ...
})

describe 可以用来进行用例分组, 同时,在每个 describe 中都有上面 4 个钩子函数的存在:

describe("测试 Button 组件", () => {
  beforeAll(...)  // 1
  beforeEach(...) // 2
  afterEach(...)  // 3
  afterAll(...)   // 4

  describe("测试 Button 组件的事件", () => {
    beforeAll(...)  // 5
    beforeEach(...) // 6
    afterEach(...)  // 7
    afterAll(...)   // 8
    test("event1", ()=>{...})
  })
})

上面钩子函数的执行顺序是: 1 > 5 > 2 > 6 > 3 > 7 > 4 > 8

外部的钩子函数对 describe 内部的用例也生效,执行顺序为:先外部后内部