乐趣区

关于前端:前端高质量交付产品利器之自动化测试

前言

对客户交付高质量的产品是企业的外围指标之一,而单元测试是实现这一指标的重要伎俩之一。通过单元测试,能够确保产品的每个局部都通过了严格的测试,升高产品呈现缺点的概率,进步产品的可靠性和稳定性。同时,单元测试的后果能够为客户提供更加精确的产品质量报告,帮忙客户更好地理解产品的长处和缺点。此外,单元测试还能够进步开发人员的信念和积极性,促成团队的单干和翻新,为客户提供更加优质的产品和服务。

单元测试是客户交付高质量产品的重要保障之一,企业应该高度重视单元测试工作,不断完善和优化测试流程和办法。

然而作为一个前端开发者来说,咱们所承当的不单单只是保障开发工作的实现,在交付所实现的我的项目之前更要保障的是品质问题,如何保障交付的品质是一个很值得探讨的问题,大多数开发者在开发过程中会针对以后所开发的内容进行自测,然而防止不了会有一些疏漏或者测试不到位的中央,导致一些很常见的 bug 的的呈现。也可能对于一个办法或者组件的调整导致援用其办法或组件的其余组件受到影响而没有进行测试导致交付的bug

所以单元测试还是十分有必要的,然而更多的时候缺疏忽了单元测试的重要性,一个残缺的我的项目单元测试的存在还是十分重要的,这篇博客将会带你重新认识单元测试,另一不便将教会你从零开始搭建环境和如何应用单元测试。

为什么要写单元测试

  1. 必要性:JavaScript短少类型查看,编译期间无奈定位到谬误,单元测试能够帮忙你测试多种异常情况。
  2. 正确性:测试能够验证代码的正确性,在上线前做到心里有底。
  3. 自动化:通过 console 尽管能够打印出外部信息,然而这是一次性的事件,下次测试还须要从头来过,效率不能失去保障。通过编写测试用例,能够做到一次编写,屡次运行。
  4. 保障重构:互联网行业产品迭代速度很快,迭代后必然存在代码重构的过程,那怎么能力保障重构后代码的品质呢?有测试用例做后盾,就能够大胆的进行重构。

单元测试和端对端测试

对于大多数开发者来说对于 Unit(俗称:单元测试) 可能据说的比拟多,对于 E2E(端到端测试) 不是那么特地的理解。那么两者之间有什么区别呢?

单元测试

单元测试 (Unit) 是站在程序员的角度测试,unit 测试 是把代码看成一个一个的组件,从而实现每一个组件的独自测试,测试内容次要是组件内的每一个函数的执行后果或返回值是否和测试中断言的后果统一。

端对端测试

端对端测试 (E2E) 是把咱们的程序堪称是一个黑盒子,我不懂你外部是怎么实现的,我只负责关上浏览器,把测试内容在页面上输出一遍,看是不是我想要失去的后果。

unit测试是程序员写好本人的逻辑后能够很容易的测试本人的逻辑返回的是不是都正确。e2e代码是测试所有的需要是不是都能够正确的实现,而且最终要的是在代码重构,js改变很多之后,须要对需要进行测试的时候测试代码是不须要扭转的,你也不必放心在重构后不能达到客户的需要。

单元测试准则

在编写单元测试时,咱们须要 mock 掉那些依赖于内部零碎或库的组件,例如数据库、网络申请等。这样能够确保测试单元独立于外部环境和其余单元的状态,只测试以后单元的性能。而对于那些依赖于外部办法或类的组件,则能够间接进行测试,因为它们是以后单元的一部分。

对于 ComposeApi 模块,咱们应该为其编写独立的单元测试,并应用实在的实现进行测试,因为它是被其余使用者所调用的。这样能够确保 ComposeApi 模块的代码品质和可维护性,并且在应用 ComposeApi 模块时,能够保障其性能失常。而对于应用 ComposeApi 模块的其余模块,在编写单元测试时,应该将其依赖 Mock 掉,以确保测试只关注以后模块的逻辑,不受其余依赖的影响。这样能够确保测试单元独立于外部环境和其余单元的状态,只测试以后模块的性能。

筹备工作

在进行单元测试之前,须要抉择适宜本人的测试工具。本文采纳 Jest 作为测试工具,因为 Jest 反对断言和覆盖率测试,具备写法简略、功能强大等长处。应用 Jest 能够帮忙咱们更好地进行单元测试,进步代码品质和可靠性。

开始搭建环境咱们应该先对以下知识点有所理解:

  1. vite
  2. typescript
  3. vue3

搭建单元测试环境

这里默认你曾经有一了一个能够增加测试环境的我的项目(如果没有请自行创立)。

首先装置对应的依赖:

yarn add jest --dev                  
yarn add @types/jest --dev          
yarn add babel-jest --dev          
yarn add @babel/preset-env --dev
yarn add @vue/vue3-jest --dev 
yarn add ts-jest --dev
yarn add @vue/cli-plugin-unit-jest --dev
yarn add @vue/test-utils@next --dev
yarn add @babel/preset-typescript --dev
yarn add babel-plugin-transform-vite-meta-env --dev
yarn add jest-environment-jsdom --dev
yarn add babel-plugin-transform-import-meta --dev

注:这里应用的是 yarn 装置的依赖,不要过于纠结包管理工具,依据本人的爱好自行抉择即可。

依赖装置实现之后,在 src 目录下创立 tests 文件夹,这个文件夹用来寄存对于测试相干的文件。

在根目录创立 jest.config.js 对 Jest 进行初始化的根本配置:

module.exports = {
  cache: false,
  collectCoverage: true,
  //  babel 预设
  transform: {
    "^.+\\.vue$": "@vue/vue3-jest",       //  反对导入 Vue 文件
    "^.+\\.jsx?$": "babel-jest",    //  反对 import 语法
    '^.+\\.tsx?$': 'ts-jest',       //  反对 ts
    '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub' //  反对导入 css 文件
  },
  moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'vue'],
  //  门路别名
  moduleNameMapper: {'^@/(.*)$': '<rootDir>/src/$1',
  },
  testEnvironment: 'jsdom',
  testEnvironmentOptions: {customExportConditions: ['node', 'node-addons'],
  },
  testMatch: ['**/tests/**/*.test.[jt]s?(x)',
  ],
  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel'
};

因为这里应用的是 TypeScript 也须要对 tsconfig.json 进行调整,让测试文件也能够反对TypeScript:

//  省略了其余配置项,其余配置依据我的项目要求自行配置即可
{
  "compilerOptions": {
    //  ...
    "types": ["vite/client", "jest"]    //  指定类型为 jest
  },
  "include": [
    // ...
    "tests/**/*.ts"     // 指定单元测试门路
  ]
}

因为 Node 无奈运行 TypeScript 这里须要应用 babelTypeScript进行编译,要配置 babel 的相干配置,在根目录创立babel.config.js

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {node: "current"}
      }
    ]
  ],
  plugins: [
    '@babel/plugin-transform-arrow-functions',
    'babel-plugin-transform-vite-meta-env',
    ['babel-plugin-transform-import-meta', { module: 'ES6'}]
  ],
};

对环境配置实现之后为了不便调用测试命令,能够在 package.jsonscripts增加快捷指令:

{
  "scripts": {
    //  ...
    "test": "jest"
  },
}

罕用断言

  1. toBe(): 测试具体的值
  2. toEqual(): 测试对象类型的值
  3. toBeCalled(): 测试函数被调用
  4. toHaveBeenCalledTimes(): 测试函数被调用的次数
  5. toHaveBeenCalledWith(): 测试函数被调用时的参数
  6. toBeNull(): 后果是 null
  7. toBeUndefined(): 后果是 undefined
  8. toBeDefined(): 后果是 defined
  9. toBeTruthy(): 后果是 true
  10. toBeFalsy(): 后果是 false
  11. toContain(): 数组匹配, 查看是否蕴含
  12. toMatch(): 匹配字符型规定, 反对正则
  13. toBeCloseTo(): 浮点数
  14. toThrow(): 反对字符串, 浮点数, 变量
  15. toMatchSnapshot(): jest 特有的快照测试
  16. not.toBe(): 后面加上.not 就是否定模式

创立第一个单元测试

对单元测试环境配置实现之后,接下来能够增加测试文件了:

// test/index.test.ts

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

增加实现之后执行命令:

yarn test

看到控制台输入:

 PASS  tests/index.test.ts
 
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |       0 |        0 |       0 |       0 |                   
----------|---------|----------|---------|---------|-------------------

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.798 s
Ran all test suites.
✨  Done in 5.32s.

模块测试

这里的模块而言次要针对的是页面,也就是常说的 router 局部,这部分次要包含组件的自定义事件以及整体 UI 性能正确性验证。

页面构造测试

通过 mountshallowMountfindfindAll 办法都能够返回一个包裹器对象,包裹器会裸露很多封装、遍历和查问其外部的 Vue 组件实例的便捷的办法。

findfindAll 办法都能够都承受一个选择器作为参数,find办法返回匹配选择器的 DOM 节点或 Vue 组件的 WrapperfindAll 办法返回所有匹配选择器的 DOM 节点或 Vue 组件的 WrappersWrapperArray

情景构想:

Home 下有一个 p 标签,class.outer,外面的内容为Aaron

import Home from "@/pages/Home.vue";
import {mount} from "@vue/test-utils";

describe('Test for Home Page', () => {

    let wrapper;

    beforeEach(() => {wrapper = shallow(Home);
    });

    it('get tag dom', () => {
        // 应用 Vue 组件选择器
        expect(wrapper.is(Test1)).toBe(true);
        // 应用 CSS 选择器
        expect(wrapper.is('.outer')).toBe(true);
        // 应用 CSS 选择器
        expect(wrapper.contains('p')).toBe(true)
        // 内容是否正确
        const contentText = wrapper.find('p').text();
        expect(contentText).toBe("Aaron")
    });
    
    it('has tag', () = > {//  isEmpty():断言 Wrapper 并不蕴含子节点。expect(wrapper.find("button").isEmpty()).toBeFalsy();
        //  exists():断言 Wrapper 或 WrapperArray 是否存在。expect(wrapper.findAll('img').exists()).toBeFalsy()});
    
    it('has className', () = > {// attributes():返回 Wrapper DOM 节点的个性对象
        expect(wrapper.find('p').attributes().class).toContain('outer');
        // classes():返回 Wrapper DOM 节点的 class 组成的数组
        expect(wrapper.find('p').classes()).toContain('outer');
    });
    
    it('has style', () = > {
        // hasStyle:判断是否有对应的内联款式
        expect(wrapper.find("p").hasStyle('padding-top', '10')).toBeTruthy()});
});
路由测试

路由是基于浏览器环境而言的,在单元测试中是没有方法失常应用路由的,须要通过 jest.mock 办法模拟出 vue-router 环境。

jest.mock("vue-router", () => {const realModule = jest.requireActual("vue-router");
  const mockRouter = {
    ...realModule,
    currentRoute: {},
    useRouter: () => ({push(route: any) {mockRouter.currentRoute.value = route;},
      currentRoute: mockRouter.currentRoute
    })
  };
  return mockRouter;
});

在上述代码中,通过函数所返回的对象,模仿了一个 vue-router 的环境,并当环境触发 push 操作的时候更改了 currentRoute 的值。

情景构想:

假如当初有两个路由配置,别离是 HomeAbout,在 Home 页面中有一个按钮,点击之后跳转到 About 页面。

当初基于现有情景去写单元测试,具体代码如下:

import Home from "@/pages/Home.vue";
import {useRouter} from 'vue-router';

test("Home.vue GoAbout", async () => {const wrapper = mount(Home as any);
  const oBtn1 = wrapper.find("#btn1");
  await oBtn1.trigger("click");
  const router = useRouter();
  const routerName = router.currentRoute?.value?.name;
  expect(routerName).toBe("About");
});

这里须要留神,在代码中调用 push 时所传递的参数会影响到最终的断言后果。如上代码,在正式环境代码中调用 push 时所传入的是 push({name: "About"}),所以在最终断言时应用About 为最终须要断言的后果。

状态治理测试

状态管理工具有很多,本文中所应用的状态管理工具是pinia,须要对其进行装置。

# npm
npm install pinia -S

# yarn
yarn add pinia -S

pinia曾经内置了单元测试的 mock 环境,咱们只须要简略的配置一下即可。

import {setActivePinia, createPinia} from 'pinia';

beforeEach(() => {setActivePinia(createPinia());
});

情景构想:

Home 下有一个 button 点击之后须要更改 pinia 中的一个值 (fooValue),更改后的值为 张三

当初基于现有情景去写单元测试,具体代码如下:

import Home from "@/pages/Home.vue";
import {useHomeStore} from "@/store/module/Home";

test("Home.vue Change Pinia", () => {const homeStore = useHomeStore();
  const vm = mount(Home as any);
  const oBtn = vm.find("#btn");
  oBtn?.trigger("click");
  expect(homeStore.fooValue).toBe("Aaron");
});

留神这里须要吧对应的 store 文件引入进来,才能够读取到更改后的值。

数据申请测试

在开发中更多的需要是当点击某一个按钮时去登程一个申请或者去该变一个值。这种状况通常也是通过模仿的形式解决。

情景构想:

Home 下有一个 button 点击之后须要进行一个申请 ajax 或者一个异步操作,去验证最初所失去的值是不是所须要的。

当初基于现有情景去写单元测试,具体代码如下:

import Home from "@/pages/Home.vue";

it('Home onGetAjax', (done) => {
  const wrapper = mount(Home as any, {setup() {const data = ref({});
      const onGetAjax = async () => {data.value = mockData.data;}
      return {onGetAjax, data}
    }
  });
  wrapper.find('#btn3').trigger('click');
  wrapper.vm.$nextTick(async () => {await wrapper.vm.onGetAjax();
    expect(wrapper.vm.data).toEqual(mockData.data)
    done();});
});

上述 onGetAjax 中要模仿所有的数据处理操作,当然这里也能够应用 axios 进行实在的数据申请。除了这种形式还有另外一种形式解决。

import Home from "@/pages/Home.vue";
//  quantityBaseList 最终的 mock 数据
import {quantityBaseList} from "./quantityBase.mock";

const ajaxMock = (vai) => vai();

const getData = () => {return new Promise((res) => {res(quantityBaseList)
  })
};

jest.mock("axios", () => {
  const mAxiosInstance = {get: jest.fn()
  };
  return {create: jest.fn(() => mAxiosInstance),
  };
});

// 这里必须要写
// 须要在这里 mock 一下申请数据的函数
jest.mock('@/api/Home', () => { 
  return {getCreateCustomerData: jest.fn(getData)
  }
});

it("Home getTabeList", () => {return wrapper.vm.$nextTick(async (res) => {const result = await ajaxMock(getData);
    expect(result.data.items).toEqual(quantityBaseList.data.items);
    expect(result.data.totalCount).toEqual(quantityBaseList.data.totalCount);
  });
});

上述代码间接 mock 了申请数据的办法,当数据申请实现之后所须要的后续操作进行断言也是能够的。

组件测试

单元测试环境配置胜利,接下来也就是重中之重的环节对组件进行测试。单元测试因为是跑在 node 环境中的,所以很多状况不能间接去应用实在开发环境中的内容测试,所以很多状况须要在做单元测试是手动模仿。

组件参数测试

下面说了很多对于页面相干的,除了页面之外也会对组件进行相干的测试。对于组件的话最常见的可能就是 props 参数。

情景构想:

一个组件须要接管一个 propmodel<object>,只须要测试一下这个 model 是否能够失常接管。

let model = ref({name: "张三"});

it("provide/inject", () => {
 let parent = mount(Detail as any, {props: { model},
  });
  expect(child.vm.parentProps?.model).toEqual(model);
});
自定义事件测试

自定义事件个别指的是,当组件外部执行完某一事件之后,须要告诉上一层做出一些响应操作,零碎开发中会常常用到。

情景构想:

Home 下有一个 Foo 的组件,Foo中有一个按钮当这个按钮点击的时候 Home 须要执行一个函数。

import Home from "@/pages/Home.vue";
import Foo from "@/components/Foo/index.vue";

describe('Test for Foo Component', () => {wrapper = mount(Home as any);
 
 it('addCounter Fn should be called', () = > {const mockFn = jest.fn();
    wrapper.setMethods({'addCounter': mockFn});
    wrapper.find(Foo).vm.$emit('add', 100);
    expect(mockFn).toHaveBeenCalledTimes(1);
 });
 
 wrapper.destroy()});

这里应用了 $on 办法,将 Home 自定义的 add 事件替换为 Mock 函数,对于自定义事件,不能应用 trigger 办法触发,因为 trigger 只是用 DOM 事件。自定义事件应用 $emit 触发,前提是通过 find 找到 Foo 组件。

计算属性测试

计算属性是一个数据, 依赖另外一些数据计算而来的后果,当一个变量的值, 须要用另外变量计算而得来。对于计算属性的利用场景也是蛮多的。

情景构想:

FooText 组件中,有一个 input 标签,通过计算属性把 input 输出的值进行反转,并输入到了组件中一个 p 标签中。

import FooText from "@/components/FooText/index.vue";

describe('Test for Foo Component', () => {beforeEach(() => {wrapper = shallow(FooText);
    });
    
    afterEach(() => {wrapper.destroy()
    });

    it('test computed', () => {
        //  能够通过 setProps 设置 props 属性
        wrapper.setProps({needReverse: false});
        wrapper.vm.inputValue = 'ok';
        expect(wrapper.vm.outputValue).toBe('ok');
    });
    
});
数据监听测试

当一个值发生变化时须要执行一些其余操作,个别用于组件封装时,当内部数据发生变化之后,组件外部须要对组件状态进行调整。

情景构想:

FooText 组件中,通过 watch 监听 inputvalue的值变动后须要执行一个函数。

import FooText from "@/components/FooText/index.vue";

describe('Test watch', () = > {
    let spy;
  
    beforeEach(() = > {wrapper = shallow(FooText);
        spy = jest.spyOn(console, 'log');
    });
    
    afterEach(() = > {wrapper.destroy();
        spy.mockClear()});
    
    it('is called with the new value in other cases', () = > {
        // 对 inputValue 赋值时 spy 会被执行一次,所以须要革除 spy 的状态
        // 革除已产生的状态
        spy.mockClear();
        wrapper.vm.inputValue = 'ok';
        return wrapper.vm.$nextTick(() = > {expect(spy).not.toBeCalled();});
    });
};
依赖注入测试

我的项目中会用到 provide/inject 这种依赖注入的状况,这种状况个别会波及到父子组件嵌套的状况。

情景构想:

两个组件别离是 DetailDetailItemDetail组件中通过 provide 向下传入了 model,须要测试DetailItem 通过 inject 接管到的内容是否是父组件传入的。

import Detail from "@/components/Detail/index.vue";
import DetailItem from "@/components/DetailItem/index.vue";

it("provide/inject", () => {
    let parent = mount(Detail, {props: { model},
      slots: {default: DetailItem}
    });
    let child = parent.findComponent(DetailItem);
    expect(child.vm.parentProps?.model).toEqual(model);
});
插槽测试

对于 slot 的测试也是必不可少的,特地有的时候时候 slot 所裸露进去的参数是否正确,也是让人关系的。

情景构想:

DetailItem组件能够应用 slot 对内容进行渲染,slot裸露进去了 value 参数以供渲染应用,测试 value 是否正确。

it("render slots", () => { 
    const wrapper = mount(DetailItem, {
      global: {
        provide: {
          "detail": reactive({
            model,
            align: "left",
            labelWidth: "100px"
          })
        },
      },
      slots: {default: (item) => `<b>${item.value}</b>`
      },
      props: {
        label,
        prop,
        align: "left",
        labelWidth: "100px"
      }
    });
    expect(`<b>${model.value[prop]}</b>`).toBe(wrapper.find(".w-full").text())
})

测试报告阐明与应用

sonarQube是一款代码品质管理工具,能够用于查看代码品质、安全性和可维护性等方面。它反对多种编程语言和技术栈,并提供了丰盛的指标和报告,帮忙开发团队更好地治理和优化代码品质。

对于前端我的项目,sonarQube能够应用前端的测试报告来评估代码品质。首先,须要在前端我的项目中配置好测试框架,并生成测试报告。将测试报告导入到 sonarQube 中,以便 sonarQube 能够读取并剖析它们。sonarQube反对多种测试报告格局,例如 JUnitSurefireCobertura 等。能够依据前端测试框架生成的测试报告格局,抉择相应的 sonarQube 测试报告插件进行导入。

sonarQube 中配置好前端我的项目的查看项和指标,例如代码覆盖率、代码复杂度、代码反复率等。sonarQube会依据测试报告和查看项,生成相应的代码品质报告和指标剖析。开发团队能够依据这些报告和指标,优化和治理前端代码品质。

当执行完 单元测试 之后会在我的项目根目录下生成一个名为 coverage 的文件夹,文件夹中 clover.xmllcov.info寄存的即是覆盖率相干的内容。

sonar-project.properties的运行命令中增加:

// xml 的地位
sonar.testExecutionReportPaths=test/covrage/test-report.xml   
// lcov.info 的地位
sonar.javascript.lcov.reportPaths=test/covrage/lcov.info 

jest执行完会生成一个覆盖率统计表, 所有在覆盖率统计文件夹下的文件都会被检测, 覆盖率指标:

  • File:文件门路
  • Statements: 语句覆盖率, 执行到每个语句
  • Branches: 分支覆盖率, 执行到每个 if 代码块
  • Functions: 函数覆盖率, 调用到程式中的每一个函数
  • Lines: 行覆盖率, 执行到程序中的每一行
------------------|---------|----------|---------|---------|-------------------
File              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s                                          
------------------|---------|----------|---------|---------|-------------------
All files         |   63.95 |    56.42 |   70.31 |   67.68 |                                                            
 src/components   |   68.84 |    65.11 |   67.85 |    73.8 |                                                            
  Foo.vue         |   67.91 |    65.11 |   66.66 |   73.17 | 
------------------|---------|----------|---------|---------|-------------------

总结

单元测试是进步软件开发品质的重要伎俩,它能够帮忙企业进步产品质量和客户满意度,从而为企业的长期倒退奠定松软的根底。通过单元测试,企业能够及时发现并解决代码中的问题,进步代码的可维护性和可扩展性,升高代码保护老本和危险,为将来的开发工作提供更好的根底。同时,单元测试还能够进步开发人员的信念和积极性,促成团队的单干和翻新,为企业发明更多的价值和竞争劣势。

在进行单元测试时,须要留神的是单元测试并不是一项简略的工作,须要测试者具备肯定的技术和教训。测试者须要充分考虑测试用例的覆盖率和测试后果的准确性,确保测试的有效性和可靠性。此外,测试者还须要理解我的项目需要和性能,依据理论状况进行测试设计和测试用例编写,从而保障测试的全面性和完整性。

总之,单元测试是软件开发过程中不可或缺的一环,它能够帮忙企业进步软件开发品质和客户满意度,为企业的长期倒退奠定松软的根底。因而,企业应该高度重视单元测试工作,踊跃推广单元测试理念,一直进步测试程度和技术能力,为企业的将来倒退注入新的能源和生机。

退出移动版