Jest前端自动化测试框架难点进阶

一、snapshot快照测试

新建 lesson9.js

export const generateConfig = () => {
  return {
    server: 'http://localhost',
    port: 8080
  }
}

新建 lesson9.test.js

import { generateConfig } from "./lesson9";

test('测试 generateConfig', async () => {
    expect(generateConfig()).toEqual({
      server: 'http://localhost',
      port: 8080
    })
})

对于这种配置文件最简单的测试思路就是如上面这样写,但是这样的话每次配置文件有改动都需要相应修改测试案例,并不友好,因此快照功能应运而生!!!

修改 lesson9.test.js

test('测试 generateConfig', async () => {
  expect(generateConfig()).toMatchSnapshot();
})

运行,日志如下:

 PASS  ./lesson9.test.js
  √ 测试 generateConfig (6ms)1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 written, 1 total
Time:        3.697s

toMatchSnapshot: 匹配快照,首次执行会在测试案例文件所在目录下生成一个__snapshots__目录,目录下有个文件lesson9.test.js.snap:。

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`测试 generateConfig 1`] = `
Object {
  "port": 8080,
  "server": "http://localhost",
}
`;

若是下次测试生成快照文件与之前不同,则测试不会通过,试一下:
修改 lesson9.js:

export const generateConfig = () => {
  return {
    server: 'http://localhost',
    port: 8080,
    alisa: {}
  }
}

运行测试用例,日志如下:

FAIL  src/lesson9/__tests__/lesson9.test.js
  × 测试 generateConfig (10ms)

  ● 测试 generateConfig

    expect(received).toMatchSnapshot()

    Snapshot name: `测试 generateConfig 1`

    - Snapshot
    + Received

      Object {
    +   "alisa": Object {},
        "port": 8080,
        "server": "http://localhost",
      }

      2 |
      3 | test('测试 generateConfig', async () => {
    > 4 |   expect(generateConfig()).toMatchSnapshot();
        |                            ^
      5 | })

      at Object.<anonymous> (src/lesson9/__tests__/lesson9.test.js:4:28)1 snapshot failed.
Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with `-u` to update them.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        4.32s

失败,快照不匹配,若是确定要修改,且在命令行模式,可以输入u更新快照,若是一个文件中只能有一个包含多个快照 需要输入 i 进行交互式的一个个对比更新。最后回车返回即可。

在这里插入图片描述
不过这样的配置文件都是写死的,若是配置文件内容是动态变化的比如:
修改 lesson9.js

export const generateConfig = () => {
  return {
    server: 'http://localhost',
    port: 8080,
    time: new Date()
  }
}

这样的话每次生成的快照一定是不一样的,也就通不过测试了。
修改 lesson9.test.js:

test('测试 generateConfig', async () => {
  expect(generateConfig()).toMatchSnapshot({
    time: expect.any(Date)
  });
})

这样的话 time 只会对比类型,不会对比值,测试就可以通过啦。

下面来试一下行内快照:

安装 prettier:

npm i prettier@1.18.2 -S

修改 lesson9.test.js(toMatchSnapshot 改为 toMatchInlineSnapshot):

test("测试 generateConfig", async () => {
  expect(generateConfig()).toMatchInlineSnapshot({
    time: expect.any(Date)
  });
});

运行测试用例,日志如下:

PASS  src/lesson9/__tests__/lesson9.test.js
  √ 测试 generateConfig (27ms)

 › 1 snapshot written.
 › 1 snapshot obsolete.
   • 测试 generateConfig 1
Snapshot Summary
 › 1 snapshot written from 1 test suite.
 › 1 snapshot obsolete from 1 test suite. To remove it, re-run jest with `-u`.
   ↳ src/lesson9/__tests__/lesson9.test.js
       • 测试 generateConfig 1

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 obsolete, 1 written, 1 total
Time:        4.151s

运行过后 lesson9.test.js 变了样子:

test("测试 generateConfig", async () => {
  expect(generateConfig()).toMatchInlineSnapshot(
    {
      time: expect.any(Date)
    },
    `
    Object {
      "port": 8080,
      "server": "http://localhost",
      "time": Any<Date>,
    }
  `
  );
});

可以看到快照作为第二个参数被保存到了测试案例里。

二、mock深入学习

新建 lesson10.js

import axios from "axios";

const fetchData = () => {
  return axios.get("/").then((res) => res.data);
};

export { fetchData };

新建 lesson10.test.js

import { fetchData } from "./lesson10.js";
import Axios from "axios";
jest.mock("axios");
test("fetchData 测试", () => {
  Axios.get.mockResolvedValue({
    data: "(function(){return '123'})()",
  });
  return fetchData().then((data) => {
    expect(eval(data)).toEqual("123");
  });
});

在jest基础入门章节中我讲过关于mock的基本知识。如上就是该知识的的体现。

如果我们不想用上面的方式去模拟接口效果,我们可以单独新建一个文件来进行模拟。

新建__mocks__文件夹,在内部新建lesson10.js

import { fetchData } from "./lesson10.js";
import Axios from "axios";
jest.mock("axios");
test("fetchData 测试", () => {
  Axios.get.mockResolvedValue({
    data: "(function(){return '123'})()",
  });
  return fetchData().then((data) => {
    expect(eval(data)).toEqual("123");
  });
});

修改 lesson10.test.js:

jest.mock("./lesson10");
//这时候会去__mocks__文件夹的lesson10.js中去找fetchData
import { fetchData } from "./lesson10.js";
test("fetchData 测试", () => {
  return fetchData().then((data) => {
    expect(eval(data)).toEqual("123");
  });
});

还有一个方法是jest.unmock(“./lesson10”);表示取消对某个文件的模拟

下面来看看jest.config.js配置文件下的automock属性:
在这里插入图片描述
打开该属性改为true:
在这里插入图片描述
修改 lesson10.test.js(注释掉模拟代码):

//jest.mock("./lesson10");
//这时候会去__mocks__文件夹的lesson10.js中去找fetchData
import { fetchData } from "./lesson10.js";
test("fetchData 测试", () => {
  return fetchData().then((data) => {
    expect(eval(data)).toEqual("123");
  });
});

重启测试。输入:npm run coverage。这时候发现测试用例也会通过。这是因为automock打开了,会自动去__mock__文件夹中找是否有fetchData,有的话就用__mock__文件夹中的fetchData来进行模拟测试。

再看一个例子:
在lesson10.js中添加一个函数:

import axios from "axios";

const fetchData = () => {
  return axios.get("/").then((res) => res.data);
};
const getNumber = () => {
  return 123;
};
export { fetchData, getNumber };

修改 lesson10.test.js:

jest.mock("./lesson10");
import { fetchData, getNumber } from "./lesson10.js";
test("fetchData 测试", () => {
  return fetchData().then((data) => {
    expect(eval(data)).toEqual("123");
  });
});

test("getNumber 测试", () => {
  expect(getNumber()).toEqual(123);
});

这时候重新运行:
在这里插入图片描述
之所以会失败是因为测试getNumber函数是从__mock__文件夹中的lesson10.js中拿getNumber的。

需要改成以下方式:

jest.mock("./lesson10");
import { fetchData } from "./lesson10.js";
//取真实的lesson10文件
const { getNumber } = jest.requireActual("./lesson10");

test("fetchData 测试", () => {
  return fetchData().then((data) => {
    expect(eval(data)).toEqual("123");
  });
});

test("getNumber 测试", () => {
  expect(getNumber()).toEqual(123);
});

三、mock timers

先看一个例子。
timer.js

export default (callback) => {
  setTimeout(() => {
    callback();
  }, 3000);
};

timer.test.js

import timer from "./timer.js";
test("timer 测试", () => {
  timer(() => {
    expect(2).toBe(1);
  });
});

运行之后会发现该测试用例居然通过了。因为timer是异步函数,不会等timer里面的内容执行完之后再去判断测试用例是否成功。

之前说了,可以用done来解决该问题:

import timer from "./timer.js";
test("timer 测试", (done) => {
  timer(() => {
    expect(2).toBe(1);
    done();
  });
});

这时候测试用例不通过。可以把expext中的2改为1就通过了。

但如果定时器后面的时间是几百秒,那这样子就太复杂了。可以用下面的方式:

import timer from "./timer.js";
//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
  const fn = jest.fn();
  timer(fn);
  jest.runAllTimers();
  expect(fn).toHaveBeenCalledTimes(1);
});

修改timer.js:

export default (callback) => {
  setTimeout(() => {
    callback();
    setTimeout(() => {
      callback();
    }, 3000);
  }, 3000);
};

再去执行测试:
在这里插入图片描述
测试用例不通过是因为这时候执行了两次。

改为2就可以通过了:
在这里插入图片描述
但有时候我们想只让外层的定时器执行,可以这样子:

import timer from "./timer.js";

//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
  const fn = jest.fn();
  timer(fn);
  //   jest.runAllTimers();
  jest.runOnlyPendingTimers();
  expect(fn).toHaveBeenCalledTimes(1);
});

我们可以改进一下:

import timer from "./timer.js";

//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
  const fn = jest.fn();
  timer(fn);
  //   jest.runAllTimers();
  //   jest.runOnlyPendingTimers();
  jest.advanceTimersByTime(3000); //时间快进3s
  expect(fn).toHaveBeenCalledTimes(1);
});

这时候测试通过。

修改代码:

import timer from "./timer.js";

//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
  const fn = jest.fn();
  timer(fn);
  //   jest.runAllTimers();
  //   jest.runOnlyPendingTimers();
  jest.advanceTimersByTime(2000); //时间快进2s
  expect(fn).toHaveBeenCalledTimes(1);
});

这时候不通过。因为只快进了2s,还差1s。

修改代码:

import timer from "./timer.js";

//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
  const fn = jest.fn();
  timer(fn);
  //   jest.runAllTimers();
  //   jest.runOnlyPendingTimers();
  jest.advanceTimersByTime(6000); //时间快进6s
  expect(fn).toHaveBeenCalledTimes(1);
});

这时候不通过。因为快进了6s,这时候是执行了两次的。

修改代码:

import timer from "./timer.js";

//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
  const fn = jest.fn();
  timer(fn);
  //   jest.runAllTimers();
  //   jest.runOnlyPendingTimers();
  jest.advanceTimersByTime(3000); //时间快进3s
  expect(fn).toHaveBeenCalledTimes(1);
  jest.advanceTimersByTime(3000); //时间快进3s
  expect(fn).toHaveBeenCalledTimes(2);
});

这时候是通过的。

由上面可知,我们会发现如果有多个test进行toHaveBeenCalledTimes,可能会影响其它的测试用例。这时候我们就需要在beforeEach钩子中重置一下timer。

import timer from "./timer.js";

beforeEach(() => {
  //在该文件中遇到setTimeout这种就用假的Timer模拟
  jest.useFakeTimers();
});
test("timer 测试", () => {
  const fn = jest.fn();
  timer(fn);
  //   jest.runAllTimers();
  //   jest.runOnlyPendingTimers();
  jest.advanceTimersByTime(3000); //时间快进3s
  expect(fn).toHaveBeenCalledTimes(1);
  jest.advanceTimersByTime(3000); //时间快进3s
  expect(fn).toHaveBeenCalledTimes(2);
});

test("timer1 测试", () => {
  const fn = jest.fn();
  timer(fn);
  //   jest.runAllTimers();
  //   jest.runOnlyPendingTimers();
  jest.advanceTimersByTime(3000); //时间快进3s
  expect(fn).toHaveBeenCalledTimes(1);
  jest.advanceTimersByTime(3000); //时间快进3s
  expect(fn).toHaveBeenCalledTimes(2);
});

四、ES6中类的测试

创建一个util.js,util类中的方法逻辑很复杂:

class Util {
  a() {
    //逻辑极其复杂
  }
  b() {
    //逻辑极其复杂
  }
}
export default Util;

对a、b两个方法的测试代码在util.test.js:

import Util from "./util.js";

let util = null;
beforeEach(() => {
  util = new Util();
});

test("测试 a方法", () => {
  //  expect(util.a(1,2)).toBe('12')
});
test("测试 b方法", () => {
  //  expect(util.b(1,2)).toBe('1212')
});

之后创建demo.js:

import Util from "./util.js";
const demoFunction = (a, b) => {
  let util = new Util();
  util.a(a);
  util.b(b);
};
export default demoFunction;

在demo.js中有一个demoFunction方法,该方法中会创建一个Util实例,然后执行Util实例中的方法a和b。

于是我们对demoFunction测试。新建demo.test.js,在这个测试文件里面,我们的重点不是要测试Util实例中a、b方法的细节,而是测试demoFunction方法中的Util被执行,并且Util的a、b方法执行即可。a、b两个方法内部逻辑极其复杂,如果真正执行会十分耗费性能。所以不如模拟a、b方法:

jest.mock("./util");
// jest发现util是一个类,会自动把类的构造函数和方法变成jest.fn(),会做以下转换
// let Util = jest.fn();
// Util.a = jest.fn();
// Util.b = jest.fn();
import Util from "./util";
import demoFunction from "./demo";

test("测试 demoFunction方法", () => {
  demoFunction();
  expect(Util).toHaveBeenCalled();
  console.log(Util.mock.instances[0]);
  expect(Util.mock.instances[0].a).toHaveBeenCalled();
  expect(Util.mock.instances[0].b).toHaveBeenCalled();
});

我们会发现我们经常在上述的单元测试中用到mock,它可以提升性能。

我们也可以自己实现对Util的模拟。在__mocks__文件夹新建util.js:

let Util = jest.fn(() => {
  console.log("constructor");
});
Util.prototype.a = jest.fn(() => {
  console.log("a方法");
});
Util.prototype.b = jest.fn(() => {
  console.log("b方法");
});
export default Util;

这样子也可以测试:
在这里插入图片描述

五、DOM节点操作的测试

安装jquery:

npm install jquery --save

新建domDemo.js:

import $ from "jquery";
const addDivToBody = () => {
  $("body").append("<div/>");
};

export default addDivToBody;

新建domDemo.test.js:

import addDivToBody from "./domDemo.js";
import $ from "jquery";

test("测试 addDivToBody方法", () => {
  addDivToBody();
  addDivToBody();
  expect($("body").find("div").length).toBe(2);
});

//本来在node环境是没有dom的
//但jest在node环境下自己模拟了一套dom的api
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值