前端单元测试那些事

简介: 很长一段时间以来,单元测试并不是前端工程师应具备的一项技能,但随着前端工程化的发展,项目日渐复杂化及代码追求高复用性等,促使单元测试愈发重要,决定整个项目质量的关键因素之一

1.单元测试的意义?


  • 大规模代码重构时,能保证重构的正确性
  • 保证代码的质量,验证功能完整性


2.主流的前端测试框架了解


2.1 框架对比(主流前三)


  • Karma - 基于Node.js的JavaScript测试执行过程管理工具(Test Runner),让你的代码自动在多个浏览器(chrome,firefox,ie等)环境下运行


  • Mocha - Mocha是一个测试框架,在vue-cli中配合chai断言库实现单元测试( Mocha+chai )


  • jest -Jest 是 Facebook 开发的一款 JavaScript 测试框架。在 Facebook 内部广泛用来测试各种 JavaScript 代码


微信截图_20220511102324.png


2.2 单元测试分类


  • TDD - (测试驱动开发)侧重点偏向开发,通过测试用例来规范约束开发者编写出质量更高、bug更少的代码


  • BDD - (行为驱动开发) 由外到内的开发方式,从外部定义业务成果,再深入到能实现这些成果,每个成果会转化成为相应的包含验收标准


简单来说就是TDD先写测试模块,再写主功能代码,然后能让测试模块通过测试,而BDD是先写主功能模块,再写测试模块


2.3 断言库


断言指的是一些布尔表达式,在程序中的某个特定点该表达式值为真,判断代码的实际执行结果与预期结果是否一致,而断言库则是讲常用的方法封装起来


主流的断言库有


  • assert (TDD)


assert("mike" == user.name);


  • expect.js(BDD) - expect() 风格的断言


expect(foo).to.be("aa");


  • should.js - BDD(行为驱动开发)风格贯穿始终


foo.should.be("aa"); //should


  • chai(BDD/TDD)  - 集成了expect()、assert()和 should风格的断言


3.单元测试之 Jest 运用


Jest 是 Facebook 开源的一款 JS 单元测试框架,它也是 React 目前使用的单元测试框架,目前vue官方也把它当作为单元测试框架官方推荐 。 目前除了 Facebook 外,Twitter、Airbnb 也在使用 Jest。Jest 除了基本的断言和 Mock 功能外,还有快照测试、实时监控模式、覆盖度报告等实用功能。 同时 Jest 几乎不需要做任何配置便可使用。


我在项目开发使用jest作为单元测试框架,结合vue官方的测试工具vue-util-test


3.1 Jest 安装


npm install --save-dev jest
npm install -g jest


3.2 Jest的配置文件


(1)添加方式


  • 自动生成 Jest.config.js


npx jest --init


然后会有一些选择,根据自己的实际情况选择


微信截图_20220511102356.png


回车后会在项目目录下自动生成Jest.config.js配置文件,当然也可以选择第二种,手动创建


  • 手动创建并配置 Jest.config.js


const path = require('path');
module.exports = {
  verbose: true,
  rootDir: path.resolve(__dirname, '../../../'),
  moduleFileExtensions: [
    'js',
    'json',
    'vue',
  ],
  testMatch: [
    '<rootDir>/src/test/unit/specs/*.spec.js',
  ], 
  transform: {
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest',
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transformIgnorePatterns: ['/node_modules/'],
  collectCoverage: false,
  coverageReporters: ['json', 'html'],
  coverageDirectory: '<rootDir>/src/test/unit/coverage', 
  collectCoverageFrom: [ 
    'src/components/**/*.(js|vue)',
    '!src/main.js',
    '!src/router/index.js',
    '!**/node_modules/**',
  ],
};


配置解析:


  • testMatch - 匹配测试用例的文件
  • transform - 用 vue-jest 处理 *.vue 文件,用babel-jest 处理 *.js 文件
  • moduleNameMapper -  支持源代码中相同的 @ -> src 别名
  • coverageDirectory - 覆盖率报告的目录,测试报告所存放的位置
  • collectCoverageFrom - 测试报告想要覆盖那些文件,目录,前面加!是避开这些文件


(2)jest命令行工具


{
  "name": "test",
  "version": "1.0.0",
  "scripts": {
    "unit": "jest --config src/test/unit/jest.conf.js --coverage",
  },
  dependencies": {
     "vue-jest": "^3.0.5",
  },
  "devDependencies":{
    "@vue/test-utils": "^1.0.0-beta.13",
    "babel-core": "^7.0.0-bridge.0",
    "babel-jest": "^21.2.0",
    "jest": "^21.2.1",
  }
}


  • config - 配置jest配置文件路径


  • coverage - 生成测试覆盖率报告


  • coverage是jest提供的生成测试覆盖率报告的命令,需要生成覆盖率报告的在package.json添加--coverage参数


(3) 单元测试文件命名


以spec.js结尾命名,spec是sepcification的缩写。就测试而言,Specification指的是给定特性或者必须满足的应用的技术细节


微信截图_20220511102413.png


(4)单元测试报告覆盖率指标


执行: npm run unit


配置后执行该命令会直接生成coverage文件并在终端显示各个指标的覆盖率概览


微信截图_20220511102425.png


在网页中打开coverage目录下的index.html就可以看到具体每个组件的测试报告


微信截图_20220511102445.png


微信截图_20220511102500.png


  • 语句覆盖率(statement coverage)是否每个语句都执行了?
  • 分支覆盖率(branch coverage)是否每个函数都调用了?
  • 函数覆盖率(function coverage)是否每个if代码块都执行了?
  • 行覆盖率(line coverage) 是否每一行都执行了?


当我们完成单元测试覆盖率达不到100%,不用慌,不用过度追求100%的覆盖率,把核心的功能模块测通即可,当然如果你要设置最低的覆盖率检测,可以在配置中加入如下,如果覆盖率低于你所设置的阈值(80%),则测试结果失败不通过


//jest.config.js
coverageThreshold: {
    "global": {
      "branches": 80,
      "functions": 80,
      "lines": 80,
      "statements": 80
    }
  },


🚀官方文档


3.3 Jest的常用断言


expect(1+1).toBe(2)//判断两个值是否相等,toBe不能判断对象,需要判断对象要使用toEqual
expect({a: 1}).toEqual({a: 1});//会递归检查对象的每个字段
expect(1).not.toBe(2)//判断不等
expect(n).toBeNull(); //判断是否为null
expect(n).toBeTruthy(); //判断结果为true
expect(n).toBeFalsy(); //判断结果为false
expect(value).toBeCloseTo(0.3); // 浮点数判断相等
expect(compileAndroidCode).toThrow(ConfigError); //判断抛出异常


3.4 Jest + Vue Test Utils 测试组件实例


Vue Test Utils 是 Vue.js 官方的单元测试实用工具库,通过两者结合来测试验证码组件,覆盖各功能测试


//kAuthCode
<template>
    <p class="kauthcode">
        <span class="kauthcode_btn" v-if="vertification" @click="handleCode"> 获取验证码</span>
        <span v-else>{{timer}} 秒重新获取</span>
    </p>
</template>
<script>
export default {
  name: 'KAuthCode',
  props: {
    phone: {
      type: String,
      require: true,
    },
    type: {
      type: String,
      default: '1',
      require: true,
      validator(t) {
        return ['1', '2'].includes(t);// 1手机 2邮箱
      },
    },
    validateType: {
      type: String,
      default: '1',
      validator(t) {
        return ['1', '2', '3'].includes(t);// 1 消息 2 表单 3自定义
      },
    },
  },
  data() {
    return {
      timer: 60,
      vertification: true,
    };
  },
  methods: {
    handleCode() {
      if (!this.phone) {
        switch (this.type) {
          case '1':
            this.$Message.warning('手机号码不能为空');
            break;
          case '2':
            this.$refs.formRef.validateField('code');
            break;
          default: break;
        }
        return;
      }
      this.getCode();
    },
    getCode() {
      let response;
      switch (this.type) {
        case '1':
          response = this.$api.login.getPhoneCode({ mobileNumber: this.phone });
          break;
        case '2':
          response = this.$api.login.getEmailCode({ email: this.phone });
          break;
        default: break;
      }
      response.then(() => {
        this.$Message.success('验证码发送成功');
        this.vertification = false;
        const codeTimer = setInterval(() => {
          this.timer -= 1;
          if (this.timer <= 0) {
            this.vertification = true;
            this.timer = 60;
            clearInterval(codeTimer);
          }
        }, 1000);
      });
    },
  },
};
</script>
<style lang="less" scoped>
    .kauthcode {
        span {
            display: inline-block;
            width: 100%;
        }
    }
</style>


测试文件


// kAuthCode.spec.js
import {createLocalVue, mount, shallowMount} from '@vue/test-utils';
import KAuthCode from '@/components/common/KAuthCode.vue';
import login from '@/service/modules/login.js';
import iviewUI from 'view-design';
const localVue = createLocalVue();
localVue.use(iviewUI);
const testPhone = '18898538706';
jest.mock('@/service/modules/login.js', () => ({
    getPhoneCode: () => Promise.resolve({
        data: {
            answer: 'mock_yes',
            image: 'mock.png',
        }
    })
}))
describe('KAuthCode.vue', () => {
    const option = {
        propsData: {
            // phone: testPhone,
            type: '2'
        },
        mocks: {
            $api: {
                login
            },
        },
    };
    beforeEach(() => {
        jest.useFakeTimers();
    });
    afterEach(() => {
        jest.clearAllTimers();
    });
    const wrapper = mount(KAuthCode, option);
    it('设置手机号码', () => {
        const getCode = jest.fn();
        option.methods = {getCode};
        wrapper.find('.kauthcode_btn').trigger('click');
        expect(wrapper.vm.phone).toBe(testPhone);
    });
    it('没有设置手机号码应报错', () => {
        wrapper.setData({type:'2'});
        const status = wrapper.find('.kauthcode_btn').trigger('click');
        expect(status).toBeFalsy();
    });
});


3.5 Vue-Test-Utils API


3.5.1 Wrapper


一个 Wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法, 通过用mount(component,option)来挂载组件,得到wrapper包裹器,可通过


  • wrapper.vm 访问实际的 Vue 实例
  • wrapper.setData 修改实例
  • wrapper.find找到相应的dom并触发事件`wrapper.find('.kauthcode_btn').trigger('click');
  • propsData - 组件被挂载时对props的设置


import {createLocalVue, mount, shallowMount} from '@vue/test-utils';
import KAuthCode from '@/components/common/KAuthCode.vue';
const option = {
        propsData: {
            // phone: testPhone,
            type: '2'
        },
        mocks: {
            $api: {
                login
            },
        },
    };
const wrapper = mount(KAuthCode, option);


ps: 也可以通过shallowMount来挂载组件,区别在于shallowMount不会渲染子组件,详细区别,可以通过shallowMount和mount两个方法分别挂载同组件并进行快照测试后查看所生成文件内容


微信截图_20220511102517.png


3.5.2  CreateLocalVue


返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类


import {createLocalVue, mount} from '@vue/test-utils';
import iviewUI from 'view-design';
const localVue = createLocalVue();
localVue.use(iviewUI);


3.5.3 测试钩子


beforeEach和afterEach - 在同一个describe描述中,beforeAll和afterAll会在多个it作用域内执行,适合做一次性设置


  • beforeEach(fn)  在每一个测试之前需要做的事情,比如测试之前将某个数据恢复到初始状态
  • afterEach(fn)       在每一个测试用例执行结束之后运行
  • beforeAll(fn)          在所有的测试之前需要做什么
  • afterAll(fn)             在测试用例执行结束之后运行


调用顺序: beforeAll =>  beforeEach =>  afterAll =>  afterEach


beforeEach(() => {
        jest.useFakeTimers();
    });
    afterEach(() => {
        jest.clearAllTimers();
    });


3.5.4 mock函数


三个与 Mock 函数相关的API,分别是jest.fn()、jest.spyOn()、jest.mock()


  • jest.fn() - 是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值,当然你也可以给他设置返回值、定义内部实现或返回Promise对象,如下例:


// 断言mockFn执行后返回值为name
it('jest.fn()返回值', () => {
  let mockFn = jest.fn().mockReturnValue('name');
  expect(mockFn()).toBe('name');
})
//定义jest.fn()的内部实现并断言其结果
it('jest.fn()的内部实现', () => {
  let mockFn = jest.fn((a, b) => {
    return a + b;
  })
  expect(mockFn(2, 2)).toBe(4);
})
//jest.fn()返回Promise对象
it('jest.fn()返回Promise', async () => {
  let mockFn = jest.fn().mockResolvedValue('name');
  let result = await mockFn();
  // 断言mockFn通过await关键字执行后返回值为name
  expect(result).toBe('name');
  // 断言mockFn调用后返回的是Promise对象
  expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})


  • jest.mock() - jest.mock 会自动根据被 mock 的模块组织 mock 对象。mock 对象将具有原模块的字段和方法


// kAuthCode.spec.js
    jest.mock('@/service/modules/login.js', () => ({
       getPhoneCode: () => Promise.resolve({
           data: {
               answer: 'mock_yes',
               image: 'mock.png',
           }
       })
   }))
  it('设置手机号码', () => {
       const getCode = jest.fn();
       option.methods = {getCode};
       wrapper.find('.kauthcode_btn').trigger('click');
       expect(getCode).toHaveBeenCalled()
       expect(wrapper.vm.phone).toBe(testPhone);
   });


需要用mock掉整个axios的请求,使用toHaveBeenCalled判断这个方法是否被调用就可以了


这个例子里面,我们只需关注getCode方法,其他可以忽略。为了测试这个方法,我们应该做到:


  • 我们不需要实际调用axios.get方法,需要将它mock掉
  • 我们需要测试是否调用了axios方法(但是并不实际触发)并且返回了一个Promise对象
  • 返回的Promise对象执行了回调函数


注:有时候会存在一种情况,在同个组件中调用同个方法,只是返回值不同,我们可能要对它进行多次不同的mock,这时候需要在beforeEach使用restoreAllMocks方法重置状态


mock的目的:


  • 设置函数返回值
  • 获取获函数调用情况
  • 改变原本函数的内部实现


4. ️踩坑点🏆


1.触发事件 -   假设组件库使用的是iview中对<Checkbox>提供的@change事件,但是当我们进行
wrapper.trigger('change')时,是触发不了的。<Button>的@click()和<button>的@click也是有区别的。
2。渲染问题 - 组件库提供的组件渲染后的html,需要通过wrapper.html()来看,可能会与你从控
制台看到的html有所区别,为避免测试结果出错,还应console.log一下wrapper.html()看一下实际的渲染结果



相关文章
|
1月前
|
前端开发 JavaScript 测试技术
前端测试技术中,如何提高集成测试的效率?
前端测试技术中,如何提高集成测试的效率?
|
24天前
|
前端开发 JavaScript 测试技术
前端自动化测试
前端自动化测试是通过使用工具和脚本自动执行测试用例的过程,旨在提高测试效率、减少人为错误,并确保Web应用的功能在不同环境和设备上的一致性与稳定性。
|
1月前
|
前端开发 JavaScript 测试技术
前端小白逆袭之路:如何快速掌握前端测试技术,确保代码质量无忧!
【10月更文挑战第30天】前端开发技术迭代迅速,新手如何快速掌握前端测试以确保代码质量?本文将介绍前端测试的基础知识,包括单元测试、集成测试和端到端测试,以及常用的测试工具如Jest、Mocha、Cypress等。通过实践和学习,你也能成为前端测试高手。
53 4
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
129 1
|
2月前
|
Web App开发 前端开发 安全
前端研发链路之测试
本文由前端徐徐撰写,介绍了前端测试的重要性及其主要类型,包括单元测试、E2E测试、覆盖率测试、安全扫描和自动化测试。文章详细讲解了每种测试的工具和应用场景,并提供了选择合适测试策略的建议,帮助开发者提高代码质量和用户体验。
50 3
前端研发链路之测试
|
1月前
|
前端开发 数据管理 测试技术
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第27天】本文介绍了前端自动化测试中Jest和Cypress的实战应用与最佳实践。Jest适合React应用的单元测试和快照测试,Cypress则擅长端到端测试,模拟用户交互。通过结合使用这两种工具,可以有效提升代码质量和开发效率。最佳实践包括单元测试与集成测试结合、快照测试、并行执行、代码覆盖率分析、测试环境管理和测试数据管理。
68 2
|
1月前
|
前端开发 JavaScript 数据可视化
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第26天】前端自动化测试在现代软件开发中至关重要,Jest和Cypress分别是单元测试和端到端测试的流行工具。本文通过解答一系列问题,介绍Jest与Cypress的实战应用与最佳实践,帮助开发者提高测试效率和代码质量。
47 2
|
6月前
|
JavaScript 前端开发 安全
在众多的测试工具中,Cypress以其强大的端到端测试能力和与TypeScript的完美结合,成为了前端开发者的首选
【6月更文挑战第11天】Cypress结合TypeScript,打造前端测试新体验。TypeScript增强代码可读性和稳定性,Cypress提供强大端到端测试,二者结合提升测试准确性和可靠性。通过类型定义、自定义命令和断言,优化测试代码;Cypress模拟真实用户操作、时间旅行功能及内置调试工具,确保应用功能性能。推荐前端开发者使用TypeScript+Cypress进行端到端测试。
84 2
|
2月前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。
130 1
|
2月前
|
前端开发 JavaScript 应用服务中间件
linux安装nginx和前端部署vue项目(实际测试react项目也可以)
本文是一篇详细的教程,介绍了如何在Linux系统上安装和配置nginx,以及如何将打包好的前端项目(如Vue或React)上传和部署到服务器上,包括了常见的错误处理方法。
784 0
linux安装nginx和前端部署vue项目(实际测试react项目也可以)