网站/小程序/APP个性化定制开发,二开,改版等服务,加扣:8582-36016

前言

单元测试是现代软件开发中不可或缺的一部分。它可以让开发人员更加自信地修改和重构代码,而不会破坏现有的功能。在编写组件时,单元测试可以帮助我们确保组件的正确性和可靠性,并提高代码质量。此外,单元测试还可以帮助我们快速定位和修复代码中的潜在问题和错误,从而提高回归效率,减少调试和修复错误所需的时间和成本。

Vue3中,我们可以使用一些现成的单元测试框架和工具,如Jest和Vue Test Utils。在写单元测试时,我们可以模拟各种不同的场景和输入,以测试组件在不同情况下的行为和反应。

今天,我们便来了解下如何在Vue3中进行单元测试。

准备工作

  1. 创建一个vue3项目:vue create vue-test

  2. 选择手动选择功能:

image.png 

3. 勾上unit Testing(单元测试)

image.png 4. 选好之后回车,选择Vue版本,这里选择3.x:

image.png 5. 选择一个格式化标准,分别是:

  • 只提示错误的ESlint

  • ESlint+Airbnb config:Airbnb config是由Airbnb公司开发和维护的一组ESLint配置规则和插件,用于帮助开发人员编写符合Airbnb代码风格指南的JavaScript代码

  • ESLint+Standard config:standard config是基于ESLint的一个预设,它是一组ESLint配置规则和插件,用于帮助开发人员编写符合JavaScript标准代码风格的代码

  • ESLint+prettier:Prettier是一个代码格式化工具,可以帮助开发人员自动格式化代码,使其符合一致的代码风格

可以根据自己的喜好选择。

image.png

  1. 接下来选择,什么时候应用这些规则:

  • Lint on save:保存时检查

  • Lint and fix on commit:提交时检查

我选择是全部勾上,当然你也可以根据自己的需要来勾选。

image.png 7. 选择进行测试的框架:

  • Jest和Mocha + Chai是两种不同的测试框架,这里我选择的是Jest image.png

  1. 选择Babel,ESLint配置内容放的位置:

  • In dedicated config files:单独的config文件

  • In package.json:放在package,json

我选择的是单独的config文件,您也可以根据自己的喜好选择。

image.png 

9. 回车生成项目。

Jest

如何部署Jest 单元测试

由于我们在创建项目时,勾选了单元测试,所以在项目结构中,我们已经可以看见est.config.js以及tests目录下的example.spec.js

image.png

通过运行npm run test:unit 执行example.spec.js中的测试。

image.png

接下来我们来学习下 jest 的相关配置

jest 的相关配置

首先,我们来看下jest.config.js中的内容:

module.exports = {
  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
  transform: {
    '^.+\\.vue$': 'vue-jest',
  },
}


其中:  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',表示预设的规则是typescript-and-babel的规则。 transform: {     '^.+\\.vue$': 'vue-jest',   },表示编译时,遇到.vue后缀使用vue-jest进行转换。 这里的我们要注意的重点还是preset预设的规则有哪些。

在guthub上找到vue-cli的代码,进入package目录:

image.png

再进入 @vue 目录下: image.png 找到 cli-plugin-unit-jest 目录: image.png 进入preset目录: image.png 选择typescript-and-babel image.png 可以看到:

image.png

打开jest-preset.js:

image.png 这里我们主要关注的还是defaultTsPreset的内容,找到对应目录下的defaultTsPreset内容:

module.exports = {
  testEnvironment: 'jsdom',
  moduleFileExtensions: [
    'js',
    'jsx',
    'json',
    // tell Jest to handle *.vue files
    'vue'
  ],
  transform: {
    // process *.vue files with vue-jest
    '^.+\\.vue$': vueJest,
    '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|
    webm|wav|mp3|m4a|aac|oga|avif)$':
    require.resolve('jest-transform-stub'),
    '^.+\\.jsx?$': require.resolve('babel-jest')
  },
  transformIgnorePatterns: ['/node_modules/'],
  // support the same @ -> src alias mapping in source code
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  // serializer for snapshots
  snapshotSerializers: [
    'jest-serializer-vue'
  ],
  testMatch: [
    '**/tests/unit/**/*.spec.[jt]s?(x)',
    '**/__tests__/*.[jt]s?(x)'
  ],
  // https://github.com/facebook/jest/issues/6766
  testURL: 'http://localhost/',
  watchPlugins: [
    require.resolve('jest-watch-typeahead/filename'),
    require.resolve('jest-watch-typeahead/testname')
  ]
}


我们挨着看下每个配置:

  1. testEnvironment: 'jsdom':指定测试运行环境为jsdom,jsdom是一个基于Node.js的库,可以在服务器端运行JavaScript,并模拟浏览器环境,这样在测试中使用浏览器API和DOM API便不会报错。

  2. moduleFileExtensions:指定Jest可以处理的模块文件扩展名

moduleFileExtensions: [
    'js',
    'jsx',
    'json',
    // tell Jest to handle *.vue files
    'vue'
  ]


这里表示js,jsx,json,vue为后缀的都可以处理。

  1. transform:指定 Jest 对测试文件进行转换的方式

transform: {
    // process *.vue files with vue-jest
    //这个规则告诉 Jest 使用 `vue-jest` 库来处理 `.vue` 文件。
    //`vue-jest` 是一个 Jest 插件,可以将 `.vue` 文件转换为 JavaScript 代码,
    以便 Jest 进行测试
    '^.+\\.vue$': vueJest,
    
    //这个规则告诉 Jest 使用 `jest-transform-stub` 库来处理一些静态资源文件,
    //如 `.css`, `.png`, `.svg` 等。`jest-transform-stub` 是一个 Jest 插件,
    可以将这些文件转换为一个空的模块,以便 Jest 进行测试。
    '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|
    webm|wav|mp3|m4a|aac|oga|avif)$':require.resolve('jest-transform-stub'),
    
    //这个规则告诉 Jest 使用 `babel-jest` 库来处理 `.js` 和 `.jsx` 文件。
    //`babel-jest` 是一个 Jest 插件,可以使用 Babel 来将 ES6+ 语法转换为 ES5 语法。
    '^.+\\.jsx?$': require.resolve('babel-jest')
  }


  1. transformIgnorePatterns: ['/node_modules/']:指定 Jest 忽略哪些文件的转换

  2. moduleNameMapper: {'^@/(.*)$': '<rootDir>/src/$1'}:配置模块名称的映射

  3. snapshotSerializers: ['jest-serializer-vue']:配置在进行快照测试时使用的序列化器

  4. testMatch:指定 Jest 应该运行哪些测试文件

testMatch: [
//这个规则告诉 Jest 匹配所有以 `.spec.js`、`.spec.jsx`、`.spec.ts` 或 `.spec.tsx` 
结尾的测试文件
//并且这些文件必须在 `tests/unit` 目录或其子目录下。
   '**/tests/unit/**/*.spec.[jt]s?(x)', 
   
//这个规则告诉 Jest 匹配所有以 `.test.js`、`.test.jsx`、`.test.ts` 或 `.test.tsx` 
结尾的测试文件
//并且这些文件必须在 `__tests__` 目录或其子目录下
   '**/__tests__/*.[jt]s?(x)'
 ],


  1. testURL: 'http://localhost/':

    用于指定在测试代码中使用的全局变量 window.location.href 的值。

  2. watchPlugins:指定 Jest 在监视模式下应该使用哪些插件

watchPlugins: [
  require.resolve('jest-watch-typeahead/filename'),
  require.resolve('jest-watch-typeahead/testname')
]


如何使用jest写测试用例

基础API

首先我们先了解几个基础的API:

  1. describe

  2. it

  3. test

describe 用法

语法:describe(name, fn)

describe(name, fn) 是一个将多个相关的测试组合在一起的块。 比如,现在有一个myBeverage对象,描述了某种饮料好喝但是不酸,通过以下方式测试:

const myBeverage = {
  delicious: true,
  sour: false,
};

describe('my beverage', () => {
  test('is delicious', () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test('is not sour', () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});


注意:这不是强制的,你甚至可以直接把 test 块直接写在最外层。 但是如果你习惯按组编写测试,使用 describe 包裹相关测试用例更加友好。

如果你有多层级的测试,你也可以嵌套使用 describe 块:

const binaryStringToNumber = binString => {
  if (!/^[01]+$/.test(binString)) {
    throw new CustomError('Not a binary number.');
  }

  return parseInt(binString, 2);
};

describe('binaryStringToNumber', () => {
  describe('given an invalid binary string', () => {
    test('composed of non-numbers throws CustomError', () => {
      expect(() => binaryStringToNumber('abc')).toThrow(CustomError);
    });

    test('with extra whitespace throws CustomError', () => {
      expect(() => binaryStringToNumber('  100')).toThrow(CustomError);
    });
  });

  describe('given a valid binary string', () => {
    test('returns the correct number', () => {
      expect(binaryStringToNumber('100')).toBe(4);
    });
  });
});
it

it() 函数是 Jest 提供的一个全局函数,用于定义一个测试用例。它接受两个参数:第一个参数是字符串,表示该测试用例的名称或描述;第二个参数是一个函数,表示该测试用例的实现代码。

function sum(a, b) {
return a + b;
}

it('sum function', () => {
expect(sum(1, 2)).toBe(3);
expect(sum(-1, 1)).toBe(0);
expect(sum(0.1, 0.2)).toBeCloseTo(0.3);
});
test

test() 函数是 it() 函数的别名,它们的作用和用法完全一样。

断言 expect

关于expect的API,可以看官网expect的描述。

写一个最简单的测试用例

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

//describe声明一个套件
describe("HelloWorld.vue", () => {
  // 定义一个测试用例
  it("renders props.msg when passed", () => {
    const msg = "new message";
    // 渲染HelloWorld,并传入参数
    const wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });
    // 期望渲染wrapper返回的文本内容与msg相匹配
    expect(wrapper.text()).toMatch(msg);
  });
});

钩子函数

  1. beforeEach

  2. afterEach

  3. beforeAll

  4. afterAll

关于这几个钩子函数的API,可以去看官网对它们的解释,这里就不再重复解释了。我们直接来看这几个钩子函数是何时触发的。 先看下列代码:

import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
beforeEach(() => {
  console.log("before each");
});

afterEach(() => {
  console.log("after each");
});

beforeAll(() => {
  console.log("before All");
});

afterAll(() => {
  console.log("after All");
});

//describe声明一个套件
describe("HelloWorld.vue", () => {
  // 定义一个测试用例
  it("renders props.msg when passed", () => {
    const msg = "new message";
    // 渲染HelloWorld,并传入参数
    const wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });
    // 期望渲染wrapper返回的文本内容与msg相匹配
    expect(wrapper.text()).toMatch(msg);
  });
  
  // 定义一个测试用例
  it("test add", () => {
    expect(1 + 1).toBe(2);
  });
});


打印输出结果是:

image.png

可以看出:beforeEach和afterEach是在每个测试用例执行的前后执行,而beforeAll和afterAll是在所用测试用例执行前后执行。

异步用例

如果用例中有异步代码应该怎么做呢?

  1. Promise:测试用例返回一个Promise,则Jest会等待Promise的resove状态,如果 Promise 的状态变为 rejected, 测试将会失败。

new Promise((resolve) => {
    expect(wrapper.text()).toEqual('123')
    resolve()
})


  1. Async/Await:基于Promise的异步语法糖

如果期望Promise被Reject,则需要使用 .catch 方法。 请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则,一个fulfilled状态的Promise不会让测试用例失败。

test('the data is peanut butter', async () => {  
   const data = await fetchData();  
   expect(data).toBe('peanut butter');  
});  
 
test('the fetch fails with an error', async () => {  
   expect.assertions(1);  
   try {  
       await fetchData();  
   } catch (e) {  
       expect(e).toMatch('error');  
   }  
});


  1. callback:done

it('renders props.msg when passed', async (done) => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld as any, {
      props: { msg },
    })
    setTimeout(() => {
      expect(wrapper.text()).toEqual(msg)
      done()
    }, 100)
  })


如何使用vue-test-utils测试vue3的组件

这里,我用来我本地的组件来进行测试,直接上代码,然后对代码中的几个重要的API进行说明。 在example.spec.ts中:

   // 引入@vue/test-utils中提供的两个方法
  import { shallowMount, mount } from '@vue/test-utils'
  //引入要进行测试的组件
  import SchemaForm, { NumberField } from '../../lib'
  // 编写组件测试的套件
  describe('HelloWorld.vue', () => {
    // 编写测试用例
    it('should render correct number field', () => {
      let value = 0
      //渲染组件并传参
      const wrapper = mount(SchemaForm, {
        props: {
          schema: {
            type: 'number',
          },
          value: value,
          onChange: (v) => {
            value = v
          },
        },
      })
      // 在渲染的结果里面寻找想要的子组件
      const numberField = wrapper.findComponent(NumberField)
      //断言
      expect(numberField.exists()).toBeTruthy()
    })
})<360>


这段代码中比较重要的几个API:

  1. mount和shallowMount

  2. findComponent

  3. exists

  4. toBeTruthy

mount和shallowMount

mount 函数会创建一个完整的组件实例,并且会渲染出组件的所有子组件。这个函数适用于测试一个组件的完整生命周期,包括子组件的交互和渲染结果。但是,由于需要渲染所有子组件,所以 mount 函数会消耗更多的资源,测试速度也会更慢。

shallowMount 函数则只会渲染当前组件,不会渲染任何子组件。这个函数适用于测试一个组件的行为,而不是它的子组件。因为不需要渲染所有子组件,所以 shallowMount 函数比 mount 函数更快。但是需要注意的是,如果组件的行为依赖于子组件的交互,那么 shallowMount 可能会测试不全面。

findComponent

用于在一个 Vue 组件的测试实例中查找子组件。

findComponent() 方法接受一个组件选项对象或组件名作为参数,并返回一个 Wrapper 实例,用于操作找到的子组件。

const numberField = wrapper.findComponent({ name: 'numberField' })

const numberFiled = wrapper.findComponent(NumberFiled)

exists

exists() 匹配器可以用于断言一个元素是否存在于 DOM 中,或者一个变量、对象、数组等是否定义或存在。

toBeTruthy

toBeTruthy() 是 Jest 测试框架中的一个匹配器(matcher),用于判断一个值是否为真(truthy)。 在 Jest 中,以下值会被视为真:

  • true

  • 任何非空字符串

  • 任何非零数值

  • 任何非空对象

  • 任何函数

如果一个值为上述任意一种,则 expect(value).toBeTruthy() 会返回 true,否则返回 false。

单元测试的指标

覆盖率

npm run test:unit --coverage 或者是在package.json里面将test:unit对应的命令里面添加--coverage,如:

image.png

执行之后得到:

image.png

接下来,我们挨着看一下这张表里面的字段分别代表什么意思。

File: 进行测试的文件,如schemaForm.tsx,type.ts等.

% Stmts:语句的覆盖率

% Branch:所有条件语句的测试覆盖率百分比(分支的覆盖率)。条件语句是指所有包含if、else、switch等关键字的代码块

% Funcs:函数的覆盖率,例如,如果一个JavaScript文件中包含10个函数,而测试覆盖率统计结果显示有8个函数被测试覆盖了,那么% Func覆盖率就是80%。

% Lines:行覆盖率,表示在所有代码行中,被测试覆盖的行占所有行的比例。

Uncovered Line #s:未被测试的语句有哪些




评论 0

暂无评论
0
0
0
立即
投稿
发表
评论
返回
顶部