学习vue-jest文档

代码demo


1、什么是单元测试

单元测试允许你将独立单元的代码进行隔离测试,其目的是为开发者提供对代码的信心。通过编写细致且有意义的测试,你能够有信心在构建新特性或重构已有代码的同时,保持应用的功能和稳定。

2、Vue单元测试要会什么?

  • Vue
  • Jest
  • vue的测试环境搭建,创建项目时已经安装unit Test

3、修改配置文件

3.1、支持测试test后缀的文件

因为Vue自带的配置只支持spec配置的文件,所以可以修改配置支持测试test后缀的文件
<br/>
修改jest.config.js文件,添加以下字段

testMatch: [
        "**/**tests**/**/*.[tj]s?(x)",// *test*/目录的js,ts,jsx,tsx文件 官方推荐
        "**/?(*.)+(spec|test).[tj]s?(x)"// 后缀为spec或者test的js,ts,jsx,tsx文件
    ]

让Eslint能够检测到test后缀文件,修改.eslintrc.js文件修改overrides下files字段

"**/tests/unit/**/*.spec.{j,t}s?(x)" // 修改前
"**/tests/unit/**/*.(spec|test).{j,t}s?(x)" // 修改后

3.2、支持控制台打印测试例子描述语

没描述

'没有描述语.png'

修改jest.config.js文件,添加以下字段

verbose: true // 支持控制台打印测试例子描述语

有描述

'有描述语.png'

3.3、支持控制台打印测试报告和导出测试报告

没导出报告

'有描述语.png'

修改package.json文件,script添加以下字段

"test:unit:coverage": "vue-cli-service test:unit --coverage",

可以使用下面的测试命令

yarn run test:unit:coverage // 命令作为测试需要报告的情况,
yarn run test // 命令作为常规测试

有导出报告

'测试报告.png'
'测试报告导出.png'

4、Jest的基本语法

4.1、test和expect

Jest拥有众多API,可以测试各种开发场景,其核心API是expect()和test(),每个测试用例都离不开这两个核心功能。test()函数主要用于描述一个测试用例,expect()函数是用于指示我们期望的结果。

test("1+1=2", () => {
 expect(1 + 1).toBe(2);
});

还有其他的期望结果,比如期望结果为某种数据类型 expect.any()

// 期望结果是数组类型 toEqual true
test("结果是数组类型", () => {
 expect([1, 2]).toEqual(expect.any(Array));
});

4.2、Matchers匹配器

匹配器(Matchers)是Jest中非常重要的一个概念,它可以提供很多种方式来让你去验证你所测试的返回值,匹配器可以通俗理解为相等操作。

4.2.1、相等匹配Matchers

toBe()

toBe()是测试expect的期望值是否全等于结果值。toBe()相当于js中的===

// toBe true
test("1+1=2", () => {
 expect(1 + 1).toBe(2);
});
// toBe false
test("'1'+'1'=2", () => {
 expect("1" + "1").toBe(2);
});
toEqual()

相对于toBe()的完全相等,toEqual()匹配的是内容是否相等,一般用于测试对象或数组的内容相等,不等于js中的==

// toEqual true
test("判断数组相同", () => {
 const arr = [1, 2];
 expect(arr).toEqual([1, 2]);
});
// toEqual false
test("'2'=2", () => {
 expect('2').toBe(2);
});

4.2.2、相等匹配Matchers

toBeTruthy()

测试expect的期望值是否为true,在js的if语句中会返回true

//  toBeNull true
test("判断null", () => {
 expect(null).toBeNull();
});
toBeFalsy()

测试expect的期望值是否为false,在js的if语句中会返回false

// toBeUndefined true
test("判断undefined", () => {
 expect(undefined).toBeUndefined();
});
toBeUndefined()

测试expect的期望值是否为undefined

// toBeTruthy true
test("在if中会返回true", () => {
 expect(1).toBeTruthy();
 expect([]).toBeTruthy();
 expect({}).toBeTruthy();
});
toBeNull()

测试expect的期望值是否为null

// toBeFalsy true
test("在if中会返回false", () => {
 expect(0).toBeFalsy();
 expect("").toBeFalsy();
});

4.2.3、逻辑判断Matche

not

取非相当于js中的!

// not true
test("取非", () => {
    expect(0).not.toBeTruthy();
});
toBeGreaterThan ()

大于相当于js中的>

// toBeGreaterThan true
test("大于", () => {
 expect(10).toBeGreaterThan(9);
});
toBeGreaterThanOrEqual ()

大于等于相当于js中的>=

// toBeGreaterThanOrEqual true
test("大于等于", () => {
 expect(10).toBeGreaterThanOrEqual(9);
 expect(10).toBeGreaterThanOrEqual(10);
});
toBeLessThan ()

小于相当于js中的<

// toBeLessThan true
test("小于", () => {
 expect(9).toBeLessThan(10);
});
toBeLessThanOrEqual ()

小于等于相当于js中的<=

// toBeLessThanOrEqual true
test("小于等于", () => {
 expect(9).toBeLessThanOrEqual(10);
 expect(10).toBeLessThanOrEqual(10);
});

4.2.4、浮点计算

toBeCloseTo()

toBeCloseTo()浮点计算时不能做到严格相等要使用toBeCloseTo,检测浮点数在某一精度是否相等

// toBe false
test("0.1 + 0.2 == 0.3", () => {
 expect(0.1 + 0.2).toBe(0.3);
});
// toBeCloseTo true
test("0.1 + 0.2 == 0.3 5精度", () => {
 expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
});

4.2.5、其他常用的匹配器

toMatch()

字符串匹配器 和字符串的match相同

// toMatch true
test("'sa' 包含于 'sabj'", function () {
 expect("sabj").toMatch("sa");
});
toHaveLength()

检测对象是否具有length属性或者其属性值是否具体为某个值

// toHaveLength false
test("Number类型没有length属性", () => {
 expect(2).toHaveLength(1);
});
// toHaveLength true
test("[1,2,3] 的长度为3", () => {
 expect([1, 2, 3]).toHaveLength(3);
});
toHaveProperty()

检测对象是否具有某个字段,或者对应的字段值是否正确

// toHaveProperty false
test("该对象存在字段name", () => {
 const obj = {
  a: 1,
 };
 expect(obj).toHaveProperty("name");
});

// toHaveProperty true
test("该对象存在字段name,值为test", () => {
 const obj = {
  name: "test",
 };
 expect(obj).toHaveProperty("name", "test");
});
toContain()

数组匹配器 用于判断数组中是否包含某些值,严格相等

// toContain false
test("['1',1]中存在1", () => {
 expect(["1", 2]).toContain(1);
});

// toContain true
test("['a','b']中存在a", () => {
 expect(["a", "b"]).toContain("a");
});

5、Jest测试异步代码

5.1、定时器

新建timer.js,并编写方法函数

export default (fn) => {
  setTimeout(() => {
    fn()
  }, 2000)
}

新建timer.test.js编写测试用例

test('测试timer', () => {
  timer(() => {
    expect(1+1).toBe(3)
  })
})

运行后我们发现测试用例居然通过了,在测试用例里写的是1+1=3这怎么可能会是对的呢?如果说jest没有抽风的话,那可能的原因应该就是我们写的断言没有被执行到了,在timer.js文件中进行调试看看

export default (fn) => {
 setTimeout(() => {
  console.log("timeout1");
  fn();
  console.log("timeout2");
 }, 2000);
}

执行后发现,果然在控制台中并没有把timeout打印出来,通过这个小例子我们不难发现,jest执行test方法时,从函数内部第一行执行到最后一行,当执行逻辑走到代码块最后一行时,没有异常就会返回测试成功,这个过程中不会去等待异步代码的执行结果,所以我们这样的测试方法,不管setTimeout里怎么实现,回调函数里怎么实现,都不会执行回调函数内部的逻辑。如果我们需要测试代码在真正执行了定时器里的异步逻辑后,才返回测试结果,我们需要给test方法的回调函数传入一个done参数,并在test方法内异步执行的代码中调用这个done方法,这样,test方法会等到done所在的代码块内容执行完毕后才返回测试结果,所以需修改测试用例

// false
test("测试timer 加done", (done) => {
    timer(() => {
        expect(1 + 1).toBe(3);
        done();
    });
});

这次执行测试代码后会发现,测试代码终于是报错了。同时只打印出了timeout1,可见jest在异常后的代码都不会执行。但这里我们又遇到了一个问题,设置定时器后结果真的是在2s后才返回,现在是只有一个定时器,如果以后项目中的延时有多个呢,或者说某个定时器的延迟时间很长的话,我们不是要等很长时间才能拿到结果,这就非常影响效率了。jest也考虑到了这一点,让我们可以使用fakeTimers模拟真实的定时器。这个fakeTimers在遇到定时器时,允许我们立即跳过定时器等待时间,执行内部逻辑。

// true
test("测试useFakeTimers", () => {
 jest.useFakeTimers();
 const fn = jest.fn();
 timer(fn);
 // 时间快进2秒
 jest.advanceTimersByTime(2000);
 expect(fn).toHaveBeenCalledTimes(2);
});
  • 首先,使用jest.fn()生成一个jest提供的用来测试的函数,这样我们之后回调函数不需要自己去写一个
  • 其次,使用jest.useFakeTimers()方法启动fakeTimer
  • 最后,可以通过jest.advanceTimersByTime()方法,参数传入毫秒时间,jest会立即跳过这个时间值,还可以通过toHaveBeenCalledTimes()这个mathcer来测试函数的调用次数。

5.2 异步请求

我们在开发过程中,难免会进行数据请求等异步操作,Jest也考虑到了这一点,现在我们以异步请求数据为例,来说明如何使用Jest进行异步代码测试。
<br/>
为了能够访问api还需要修改jest.config.js文件,添加以下字段

testEnvironmentOptions: {
        url: "http://localhost:8080" // 自己的URL
    }

首先新建request.js, request.test.js这两个文件,在request.js文件请求一个本地的json文件

import axios from "axios";
function requestCallback(fn) {
    axios.get("http://localhost:8080/test.json").then((res) => {
  fn(res);
    }).catch(res=>{
        fn(res);
    })
}

我们在request.test.js中,为了保证异步代码执行完毕后结束测试,和之前介绍的一样,在test的回调函数中传入done参数,在回调函数里执行done(),代码如下

test("测试回调请求", (done) => {
    request.requestCallback((res) => {
        var obj = {
            "a": 1,
            "b": 2
        };
        expect(res.data[0]).toEqual(obj);
  done();
 });
});

5.3 promise请求

我们现在改造一下request.js的代码,让它返回一个promise:

function requestPromise() {
    return axios.get('http://localhost:8080/test.json')
}

为了测试上述代码,我们request.test.js也要做一定的修改:

test('测试promise请求', () => {
    return request.requestPromise().then((res) => {
        var obj = {
            "a": 1,
            "b": 2
        };
        expect(res.data[0]).toEqual(obj);
    })
})

注意,上面的写法不需要传入done参数了,但是,需要我们使用return返回,如果不写return,那jest执行test函数时,将不会等待promise返回,这样的话,测试结果输出时,then方法将不会执行。

6、Jset中的钩子函数

Jest的钩子函数类似于Vue的生命周期函数,会在在代码执行的特定时刻,自动运行一个函数。Jest中有4个核心的钩子函数,分别为beforeAll、beforeEach、afterEach、afterAll,钩子函数均接受回调函数作为参数
创建calculator.js,创建hook.test.js

6.1、beforeAll

该钩子函数会在所有测试用例执行之前执行,通常用于进行初始化。

6.2、beforeEach

该钩子函数会在每个测试用例执行之前执行。

6.3、afterEach

该钩子函数会在每个测试用例执行之后执行

6.4、afterAll

该钩子函数会在所有测试用例执行之后执行。

class Calculator {
 constructor() {
  this.num = 1;
 }

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

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

 multiply() {
  this.num *= 2;
 }

 divide() {
  this.num /= 2;
 }
}
let calculator = null;
// beforeAll通常可用来初始化
beforeAll(() => {
    calculator = new Calculator();
    console.log("beforeAll执行");
});

beforeEach(() => {
    console.log("beforeEach执行");
});

afterAll(() => {
    console.log("afterAll执行");
});

afterEach(() => {
    console.log("afterEach执行");
});

test("测试Calculator中的add方法", () => {
    calculator.add();
    expect(calculator.num).toBe(2);
});
test("测试Calculator中的minus方法", () => {
    calculator.minus();
    expect(calculator.num).toBe(0);
});
test("测试Calculator中的multiply方法", () => {
    calculator.multiply();
    expect(calculator.num).toBe(2);
});
test("测试Calculator中的divide方法", () => {
    calculator.divide();
    expect(calculator.num).toBe(0.5);
});

7、Jest中的mock

用于测试模块外部函数是否被正确调用,一般使用以下3个特性

  • 擦除函数的实际实现
  • 捕获函数调用情况
  • 设置函数返回值

7.1 jest.fn()

首先在mock.js写一个函数:

const run = fn => {
   return fn('this is run!')
}

在mock.test.js文件中,

test('测试 jest.fn()', () => {
    const fn = jest.fn(() => {
        return 'this is mock fn 1'
    })
})

fn()函数可以接受一个函数作为参数,这个函数就是我们想要jest.fn()为我们mock的函数,而且jest.fn()可以初始化时候不传入参数.

7.1.1、mockImplementation

改变mock函数内容,只会改变一次

7.1.2、mockImplementationOnce

改变mock函数内容

test("测试 jest.fn()", () => {
    const func = jest.fn();
    func.mockImplementation(() => {
        return "this is mock fn 3";
    });
    func.mockImplementationOnce(() => {
        return "this is mock fn 1";
    });
    func.mockImplementationOnce(() => {
        return "this is mock fn 2";
    });
    const a = run(func);
    const b = run(func);
    const c = run(func);
    console.log(a);
    console.log(b);
    console.log(c);
});

我们可以看到,函数执行的结果第一次是this is mock fn 2,之后都是this is mock fn 1

7.1.3、mockReturnValueOnce()

改变函数的返回值,只会改变一次

7.1.4、mockReturnValue()

改变函数的返回值

test("测试 jest.fn()", () => {
    const func = jest.fn();
    func.mockImplementation(() => {
        return "this is mock fn 1";
    });
    func.mockImplementationOnce(() => {
        return "this is mock fn 2";
    });
    const a = run(func);
    const b = run(func);
    const c = run(func);
    console.log(a);
    console.log(b);
    console.log(c);
});
7.1.5、toBeCalled()

测试函数是否被调用

// true
test("测试 jest.fn()是否被调用", () => {
    const func = jest.fn();
    run(func);
    expect(func).toBeCalled();
});
7.1.6、toBeCalledWith()

测试函数的传参数是否符合预期

// true
test("测试 jest.fn()", () => {
    const func = jest.fn();
    const a = run(func);
    expect(func).toBeCalledWith("this is run!");
});

7.2、mock接口

很多时候,我们在前端开发过程中,后端接口还没有提供,我们需要去mock接口返回的数据
现在按照下列3个步骤:

  • 在测试文件中导入需要模拟的模块
  • 使用jest.mock()方法mock一下模块
  • 使用.mockResolvedValue()模拟数据返回
jest.mock("axios");
// true
test("测试request", () => {
    const obj = {
        a: 1,
        b: 2,
    };

    axios.get.mockResolvedValue({
        data: {
            a: 1,
            b: 2,
        },
    });
    return request.requestPromise().then((res) => {
        expect(res.data).toEqual(obj);
    });
});

8、组件测试

8.1、组件的挂载

由于前端组件化,使得UI测试变得容易很多。每个组件都可以被简化为类似于UI=fn(data)的表达式,这个表达式是一个描述UI是什么样的虚拟DOM,给这个表达式输入一些参数,就会得到UI描述的输出。

但是经过这样的抽象后还是存在一个问题,由于DOM是一个树形的结构。越处于上层的组件,其复杂度必然会随之提高。对于最底层的子组件来说,我们可以很容易得将其进行渲染并测试其逻辑的正确与否,但对于较上层的父组件来说,通常来说就需要对其所包含的所有子组件都进行预先渲染,甚至于最上面的组件需要渲染出整个 UI 页面的真实 DOM 节点才能对其进行测试,这显然是不可取的。

在vue-unit-test中就提供了shallowMount和mount 两个方法来实现组件的挂载,其中shallowMount就可以解决这个问题,它只渲染组件本身,但会保留子组件在组件中的存根。

区别这两种方法的目的在于,当我们只想对某个孤立的组件进行测试的时候,一方面可以避免其子组件的影响,另一方面对于包含许多子组件的组件来说,完全渲染子组件会导致组件的渲染树过大,这可能会影响到我们的测试速度

在组件挂载后,我们可以通过wrapper.vm访问到组件的实例,通过wrapper.vm进而可以访问到组件所有的props、data和methods等等。

import { shallowMount, mount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";

describe("HelloWorld.vue 判断组件是否挂载", () => {
    it("test shallowMount", () => {
        const wrapper = shallowMount(HelloWorld);
        // 判断组件是否挂载
        expect(wrapper.exists()).toBe(true);

        // 访问vm实例
        console.log(wrapper.vm);
    });
    it("test mount", () => {
        const wrapper = mount(HelloWorld);
        // 判断组件是否挂载
        expect(wrapper.exists()).toBe(true);
    });
});

8.2、DOM测试

8.2.1 find

在组件挂载后,无论哪种渲染方式所返回的 wrapper 都有一个.find()方法,它接受一个 selector 参数,然后返回一个对应的 wrapper 对象。而.findAll()则会返回一个类型相同的 wrapper 对象数组,里面包含了所有符合条件的子组件。与jquery中的用法类似selector 可以是CSS 选择器,除此之外也可以是 Vue 组件 或是一个 option 对象,以便于在 wrapper 对象中可以轻松地指定想要查找的节点。

除此之外,返回的包裹器内还有attributes、classes、element.style、text、html等属性用于验证。依然以以上HelloWorld.vue组件为例,如果我们要测试span标签是否有.item样式,是否有id,可以进行如下测试:

describe("HelloWorld.vue 查找第一个span标签", () => {
    it("test attribute and class", () => {
        const wrapper = shallowMount(HelloWorld);
        // 查找第一个span标签
        const dom = wrapper.find("span");
        expect(dom.classes()).toContain("item");
        expect(dom.attributes().id).toBeFalsy();
    });
});

8.2.2 trigger

通过find方法查找 DOM 元素之后,还可以通过trigger方法在组件上模拟触发某个 DOM 事件,比如 Click,Change 等等。如下例子所示,有一个点击后按钮计数的组件

<template>
  <div class="container">
    <button id="testClick" @click="changeText">{{btnText}}</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      btnText: 1,
    }
  },
  methods: {
    changeText () {
      this.btnText++
    }
  }
}
</script>

那么我们可以在以上组件的基础上撰写测试click原生事件的单元测试:

describe("HelloWorld.vue 测试click原生事件", () => {
    it("test trigger click", () => {
        const wrapper = shallowMount(HelloWorld);
        const dom = wrapper.find("#testClick");
        expect(wrapper.vm.btnText).toBe(1);
        dom.trigger("click");
        expect(wrapper.vm.btnText).toBe(2);
        // 测试dom是否更新
        expect(dom.text()).toContain(2);
    });
});

但是此时运行测试用例后发现,expect(dom.text()).toContain(2)用例报错了,接受到的值为1。这是由于Vue 会异步的将未生效的 DOM 更新批量应用,以避免因数据反复突变而导致的无谓的重新渲染。我们要用nextTick做一下延时,来等待 Vue 把实际的 DOM 更新做完。

describe("HelloWorld.vue 测试click原生事件 延迟更新",  () => {
    it("test trigger click", async () => {
        const wrapper = shallowMount(HelloWorld);
        const dom = wrapper.find("#testClick");
        expect(wrapper.vm.btnText).toBe(1);
        dom.trigger("click");
        expect(wrapper.vm.btnText).toBe(2);
        // 测试dom是否更新
        await wrapper.vm.$nextTick();
        expect(dom.text()).toContain(2);
    });
});

8.3、组件间值的传递测试

vue的组件免不了值得组件间通信,如props,emit以及slot等。在挂载后的wrapper中也提供了相应的api来进行测试。在测试组件间的变化时,最直观的测试方法就是,就是在父组件进行相关的操作,然后在测试子组件是否进行了对应的变化。但是这显然违反了单元测试的理念,我们只关心组件的输入输出,不关心组件间的联动。比如有如下的一个组件

<template>
  <div class="container">
   <span>{{name}}</span>
    <span id="age" @click="change">{{age}}</span>
  </div>
</template>

<script>
export default {
  props: ['name', 'age'],
  data () {
    return {
      myAge: 1,
    }
  },
  methods: {
    change () {
      this.$emit('change-age', this.myAge)
    }
  }
}
</script>

该组件接收由父组件传递的name,和age两个参数。点击age字段时向父组件传递change-age时间将myAge传递给父组件。

describe("HelloWorld.vue 测试组件的传值", () => {
    it("test props", () => {
        const wrapper = shallowMount(HelloWorld, {
            propsData: {
                name: "keng",
                age: "100",
            },
        });
        expect(wrapper.props().name).toBe("keng");
        expect(wrapper.props("age")).toBe("100");
    });
    it("test emit", () => {
        const wrapper = shallowMount(HelloWorld);
        const ageDom = wrapper.find("#age");
        ageDom.trigger("click");
        console.log(wrapper.emitted());
        expect(wrapper.emitted("change-age")[0]).toEqual([1]);
    });
});

在测试props时我们在挂载时利用propsData给props赋值,利用wrapper.props测试是否可以得到目标值,wrapper.props().name等价于wrapper.props('name')在对emit测试时模拟事件触发,测试目标事件是否同时触发,同时值是否也向外传递。

8.4、组件状态改变

使用setData或setProps 方法 直接操纵组件状态

describe("HelloWorld.vue 测试组件状态的改变", () => {
    it("test setData", async () => {
        const wrapper = shallowMount(HelloWorld);
        await wrapper.setData({ btnText :2});
        const dom = wrapper.find("#testClick");
        expect(dom.text()).toContain(2);
        expect(wrapper.vm.$data.btnText).toBe(2);
    });
    it("test setProps",  () => {
        const wrapper = shallowMount(HelloWorld);
        wrapper.setData({
            name: "keng",
            age: "100",
        });
        expect(wrapper.props().name).toBe("keng");
        expect(wrapper.props("age")).toBe("100");
    });
});
hxy

hxy

秦 夏

留下你的评论

快留下你的小秘密吧