前端抢饭碗系列之Vue项目中如何做单元测试(二)

发表于:2021-7-20 10:15

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:谢小飞    来源:掘金

  异步代码
  我们项目中经常也会涉及到异步代码,比如setTimeout、接口请求等都会涉及到异步,那么这些异步代码怎么来进行测试呢?假设我们有一个异步获取数据的函数fetchData:
  export function fetchData(cb) {
    setTimeout(() => {
      cb("res data");
    }, 2000);
  }
  在2秒后通过回调函数返回了一个字符串,我们可以在测试用例的函数中使用一个done的参数,Jest会等done回调后再完成测试:
  test("callback", (done) => {
    function cb(data) {
      try {
        expect(data).toBe("res data");
        done();
      } catch (error) {
        done();
      }
    }
    fetchData(cb);
  });
  我们将一个回调函数传入fetchData,在回调函数中对返回的数据进行断言,在断言结束后需要调用done;如果最后没有调用done,那么Jest不知道什么时候结束,就会报错;在我们日常代码中,都会通过promise来获取数据,将我们的fetchData进行一下改写:
  export function fetchData() {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("promise data");
      }, 2000);
    });
  }
  Jest支持在测试用例中直接返回一个promise,我们可以在then中进行断言:
  test("promise callback", () => {
    return fetchData().then((res) => {
      expect(res).toBe("promise data");
    });
  });
  除了直接将fetchData返回,我们也可以在断言中使用.resolves/.rejects 匹配符,Jest也会等待promise结束:
  test("promise callback", () => {
    return expect(fetchData()).resolves.toBe("promise data");
  });
  除此之外,Jest还支持async/await,不过我们需要在test的匿名函数加上async修饰符表示:
  test("async/await callback", async () => {
    const data = await fetchData();
    expect(data).toBe("promise data");
  });
  全局挂载与卸载
  全局挂载和卸载有点类似Vue-Router的全局守卫,在每个导航触发前和触发后做一些操作;在Jest中也有,比如我们需要在每个测试用例前初始化一些数据,或者在每个测试用例之后清除数据,就可以使用beforeEach和afterEach:
  let cityList = []
  beforeEach(() => {
    initializeCityDatabase();
  });
  afterEach(() => {
    clearCityDatabase();
  });
  test("city data has suzhou", () =>  {
    expect(cityList).toContain("suzhou")
  })
  test("city data has shanghai", () =>  {
    expect(cityList).toContain("suzhou")
  })
  这样,每个测试用例进行测试前都会调用init,每次结束后都会调用clear;我们有可能会在某些test中更改cityList的数据,但是在beforeEach进行初始化的操作后,每个测试用例获取的cityList数据就保证都是相同的;和上面一节异步代码一样,在beforeEach和afterEach我们也可以使用异步代码来进行初始化:
  let cityList = []
  beforeEach(() => {
    return initializeCityDatabase().then((res)=>{
      cityList = res.data
    });
  });
  //或者使用async/await
  beforeEach(async () => {
    cityList = await initializeCityDatabase();
  });
  和beforeEach和afterEach相对应的就是beforeAll和afterAll,区别就是beforeAll和afterAll只会执行一次;beforeEach和afterEach默认会应用到每个test,但是我们可能希望只针对某些test,我们可以通过describe将这些test放到一起,这样就只应用到describe块中的test:
  beforeEach(() => {
    // 应用到所有的test
  });
  describe("put test together", () => {
    beforeEach(() => {
      // 只应用当前describe块中的test
    });
    test("test1", ()=> {})
    test("test2", ()=> {})
  });
  模拟函数
  在项目中,一个模块的函数内常常会去调用另外一个模块的函数。在单元测试中,我们可能并不需要关心内部调用的函数的执行过程和结果,只想知道被调用模块的函数是否被正确调用,甚至会指定该函数的返回值,因此模拟函数十分有必要。
  如果我们正在测试一个函数forEach,它的参数包括了一个回调函数,作用在数组上的每个元素:
  export function forEach(items, callback) {
    for (let index = 0; index < items.length; index++) {
      callback(items[index]);
    }
  }
  为了测试这个forEach,我们需要构建一个模拟函数,来检查模拟函数是否按照预期被调用了:
  test("mock callback", () => {
    const mockCallback = jest.fn((x) => 42 + x);
    forEach([0, 1, 2], mockCallback);
    expect(mockCallback.mock.calls.length).toBe(3);
    expect(mockCallback.mock.calls[0][0]).toBe(0);
    expect(mockCallback.mock.calls[1][0]).toBe(1);
    expect(mockCallback.mock.calls[2][0]).toBe(1);
    expect(mockCallback.mock.results[0].value).toBe(42);
  });
  我们发现在mockCallback有一个特殊的.mock属性,它保存了模拟函数被调用的信息;我们打印出来看下:
  它有四个属性:
  · calls:调用参数
  · instances:this指向
  · invocationCallOrder:函数调用顺序
  · results:调用结果
  在上面属性中有一个instances属性,表示了函数的this指向,我们还可以通过bind函数来更改我们模拟函数的this:
  test("mock callback", () => {
      const mockCallback = jest.fn((x) => 42 + x);
      const obj = { a: 1 };
      const bindMockCallback = mockCallback.bind(obj);
      forEach([0, 1, 2], bindMockCallback);
      expect(mockCallback.mock.instances[0]).toEqual(obj);
      expect(mockCallback.mock.instances[1]).toEqual(obj);
      expect(mockCallback.mock.instances[2]).toEqual(obj);
  });
  通过bind更改函数的this之后,我们可以用instances来进行检测;模拟函数可以在运行时将返回值进行注入:
  const myMock = jest.fn();
  // undefined
  console.log(myMock());
  myMock
      .mockReturnValueOnce(10)
      .mockReturnValueOnce("x")
      .mockReturnValue(true);
  //10 x true true
  console.log(myMock(), myMock(), myMock(), myMock());
  myMock.mockReturnValueOnce(null);
  // null true true
  console.log(myMock(), myMock(), myMock());
  我们第一次执行myMock,由于没有注入任何返回值,然后通过mockReturnValueOnce和mockReturnValue进行返回值注入,Once只会注入一次;模拟函数在连续性函数传递返回值时使用注入非常的有用:
  const filterFn = jest.fn();
  filterFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
  const result = [2, 3].filter((num) => filterFn(num));
  expect(result).toEqual([2]);
  我们还可以对模拟函数的调用情况进行断言:
  const mockFunc = jest.fn();
  // 断言函数还没有被调用
  expect(mockFunc).not.toHaveBeenCalled();
  mockFunc(1, 2);
  mockFunc(2, 3);
  // 断言函数至少调用一次
  expect(mockFunc).toHaveBeenCalled();
  // 断言函数调用参数
  expect(mockFunc).toHaveBeenCalledWith(1, 2);
  expect(mockFunc).toHaveBeenCalledWith(2, 3);
  // 断言函数最后一次的调用参数
  expect(mockFunc).toHaveBeenLastCalledWith(2, 3);
  除了能对函数进行模拟,Jest还支持拦截axios返回数据,假如我们有一个获取用户的接口:
  // /src/api/users
  const axios = require("axios");
  function fetchUserData() {
    return axios
      .get("/user.json")
      .then((resp) => resp.data);
  }
  module.exports = {
    fetchUserData,
  };
  现在我们想要测试fetchUserData函数获取数据但是并不实际请求接口,我们可以使用jest.mock来模拟axios模块:
  const users = require("../api/users");
  const axios = require("axios");
  jest.mock("axios");
  test("should fetch users", () => {
    const userData = {
      name: "aaa",
      age: 10,
    };
    const resp = { data: userData };
    axios.get.mockResolvedValue(resp);
    return users.fetchUserData().then((res) => {
      expect(res).toEqual(userData);
    });
  });
  一旦我们对模块进行了模拟,我们可以用get函数提供一个mockResolvedValue方法,以返回我们需要测试的数据;通过模拟后,实际上axios并没有去真正发送请求去获取/user.json的数据。

      本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号