Skip to content
On this page

在vue2中进行单元测试

测试准则

测试规范

文件结构

测试文件的命名规范为:*.test.js*为需要测试的组件名称或文件名称,文件放在模块的__tests__目录下,比如短信模块的测试文件目录结构如下:

markdown
.
├─ message
│  ├─ __tests__
|     └─ New.test.js

命名

在需要测试的 dom 或组件上添加data-test属性,不要使用className来测试,因为它们可能会被删除或变动。正确的测试如下:

html
<Foo data-test="foo" class="foo" />
<button data-test="saveButton" />

相关文档

组件测试文档

jest文档

测试功能

测试起步

  • mount/shallowMount

渲染测试组件的方法,具体的区别见此处。 mount和shallowMount在注册时可以传入一些选项帮助我们完善测试环境所需要的上下文:首先让我们了解下最常用到的两个配置stubs和mocks。

  • stubs

存根,也即测试组件中所包含的子组件,一般不需要测试它们,但有时需要借助它们去抛出事件来模拟操作场景,所以按需注册即可。详细的使用文档见此处

  • mocks

用来模拟vue组件的实例方法:比如$errorHandler、$router、$message等。详细的使用文档见此处

shallowMount or mount

为什么使用shallowMount

可以遵循以下准则:能使用shallowMount的情况绝不使用mount。 那么mount一般在什么时候使用?以下是可能使用mount的场景:

  1. 测试stubs组件内的内容:由于shallowMount无法真实的渲染stubs组件,所以对于组件内的内容测试需要用过mount来渲染测试,当然这并不是说推荐我们去测试子组件的内容,而是针对父组件的测试内容涵盖了子组件渲染的部分我们需要借助mount来渲染正确的dom结构,比方说a-table/c-table下的列表操作的测试。
js
it('二维码不存在的提示', async () => {
        const record = {
            "id": "1139087491756986368",
            "remark": "11",
            "url": "https://alibaba.beta.pageseagle.com/1140565968175497216/index?source_id=1010781213482733568&source=来源名称&utm_campaign=全员营销",
            "sourceId": "1010781213482733568",
            "source": "来源名称",
            "utmSource": "",
            "utmMedium": "",
            "utmCampaign": "全员营销",
            "utmTerm": "",
            "utmContent": "",
            qrCodeId: '123',
            "assetId": "1139087491706654720",
            "createdAt": "2023-08-15T06:47:01.2575758+00:00"
        }
        const option = wrapperOption()
        GetScene.mockResolvedValue([record])
        // 设置测试中用到的stubs
        Object.assign(option.stubs, {
            'c-table': CTable,
            'a-table': Table
        })
        option.propsData = {
            id: '123',
            assetId: '456'
        }
        // 通过mount来渲染table
        const wrapper = mount(Sources, option)
        // 等待初始化的数据加载完成
        await sleep(0)
        // table中某行的操作按钮
        await wrapper.find('[data-testid=qrcodeIcon]').trigger('click')
        // 模拟组件触发事件
        wrapper.findComponent(SomeVueComponent).vm.$emit('someEvent',...someParams)
        expect(wrapper.html()).toContain('二维码被删除')
    })

定义好的工具类

  • test/mock.js

src/utils/test/mock.js中封装了wrapperMockswrapperOption,用于创建测试组件的上下文,具体的含义可以参考代码文档。

  • test/setup/request.js 异步请求的通用配置文件,根据路由规则对请求进行mock处理,详细的使用文档见此处

根据不同的数据集进行测试

  • setData / setProps
javascript
it('displays error message when content is too long', async () => {
    wrapper = shallowMount(New, {
        ...option,
        {
            // 设置props
            propsData: {
                id: 'test'
            }
        }
    })
    // 设置data(更推荐使用交互或者事件的方式代替,比如tirgger('click')、$emit('input'))
    await wrapper.setData({ content: 'a'.repeat(500) })
    await wrapper.find('button').trigger('click')
    expect(mocks.$errorHandler).toBeCalledWith(Error('内容超出最大长度限制'))
})

vue实例方法

虽然正确的做法是只专注dom输出和公共接口,但是出于测试的便利和可行性,我们可以在测试中测试是否调用了vue实例的方法来模拟某些场景(eg.路由是否发生了改变可以通过是否调用$router来判断,是否展示了错误异常可以通过是否调用了$errorHandler来判断)。

src/utils/test/mock.js中封装了wrapperOption,其中包含了组件实例的方法,比如我们要测试$errorHandler有没有被调用:

javascript
import { wrapperOption } from '@/utils/test/mock'

describe('New.vue 创建新短信', () => {
    let wrapper
    const option = wrapperOption()
    beforeEach(() => {
        wrapper = shallowMount(New, option);
    })

    it('displays error message when content is empty', async () => {
        await wrapper.find('button').trigger('click')
        expect(mocks.$errorHandler).toBeCalledWith(Error('短信内容不能为空'))
    })
})

以下是测试$router的例子

javascript
it('navigates to MessageTemplate page after saving', async () => {
    const d = {
        "content": "test",
        "signatureId": "test",
        "title": "test",
        "type": 1,
    }
    await wrapper.setData(d)
    await wrapper.find('button').trigger('click')
    expect(SaveTemplate).toBeCalledWith(d)
    await sleep(0)
    expect(mocks.$router.push).toBeCalled()
})

测试第三方组件,比如ant-deisign-vue

js
// 使用常用组件
import {wrapperStubs} from '@/utils/test/mock'
shallowMount(New, { stubs: wrapperStubs() } )
// wrapperStubs中包含了常用的测试组件,如果不满足需求,可以自定义,比如:
import {Modal} from 'ant-design-vue'
shallowMount(New, { stubs: {'a-modal': Modal} } )

处理异步请求

本项目对axios进行了统一的封装,所有请求都会返回一个空的promise.resolve对象。

  • 如果只是为了保证程序能正常运行,防止undefined之类的错误,可以在setup文件中定义:

WARNING

需要注意的是这边只是默认的返回结果以保证程序不出错,一般为空数据,不要定义具体的返回结果

javascript
// __test__/setup/request.js
const getImplementation = (route, params) => {
    if ([/campaignType$/, /configcenter\/list$/].some(reg => reg.test(route))) {
        return Promise.resolve([])
    }
    return Promise.resolve({ data: [] })
}
  • 如果测试依赖于请求的具体内容,则需要对请求进行mock处理,以下介绍了两种mock方式:
javascript
import { SaveTemplate } from '@/api/message/template'
// 短信副本
const copyTemplate = {
    type: 1,
    title: 'test',
    content: 'hhh',
    signatureId: 'test'
}
// mock api
jest.mock('@/api/message/template', () => {
    const originalModule = jest.requireActual('@/api/message/template')
    return {
        __esModule: true,
        ...originalModule,
        // 在整个测试文件使用一个测试用例
        GetTemplateById: jest.fn().mockResolvedValue(copyTemplate),
        SaveTemplate: jest.fn()
    };
})
describe('New.vue 创建短信副本', () => {
    let wrapper
    const option = wrapperOption()
    beforeEach(() => {
        wrapper = shallowMount(New, {
            ...option,
            propsData: {
                id: 'test'
            }
        });
    })

    it('创建副本后直接保存为新短信', async () => {
        await wrapper.find('button').trigger('click')
        expect(SaveTemplate).toBeCalledWith({ ...copyTemplate, title: '副本-' + copyTemplate.title })
    })

    // 在这条测试中使用特定的测试用例
    it('创建副本后直接保存为新短信', async () => {
        GetTemplateById.mockResolvedValue(copyTemplate)
        // 实例需要在mock后再注册
        const wrapper = shallowMount(New, {
            ...wrapperOption(),
            propsData: {
                id: 'test'
            }
        });
        await wrapper.find('button').trigger('click')
        expect(SaveTemplate).toBeCalledWith({ ...copyTemplate, title: '副本-' + copyTemplate.title })
    })
})
  • 等待异步请求执行完成

由于异步请求是一个微任务,所以我们在下一个宏任务中执行断言,这时候需要等待异步请求执行完成,这里提供了一个sleep方法,可以在测试中使用:

js
import { sleep } from '@/utils/common'
sleep(0)

运行完整的测试

bash
yarn test:unit:dev

测试覆盖率

查看测试覆盖率,可以按以下操作执行:

  • 把测试的文件目录添加到jest.config.json中的jest.collectCoverageFrom
json
"collectCoverageFrom": [
    // 统计message和mail文件夹下的测试覆盖率
    "src/**/{message,mail}/**/*.{js,vue}",
    "!src/utils/test/*.js",
]
  • 运行命令
bash
yarn test:coverage

QAs:

如何将文件排除在测试外

skip.的开头js或者vue文件不会包含到测试和测试覆盖率中

如何对单个测试文件进行调试

  • 手动调用
bash
yarn test:unit:dev src/components/campaign/market/__tests__/WebinarLanding.test.js
  • 配置VSCode并执行task
  1. F1唤起命令行,输入Configure Task
  2. 输入配置
json
"tasks": [
        {
            "label": "Run Test",
            "type": "shell",
            "command": "yarn",
            "args": [
                "run",
                "test:unit:dev",
                "${file}"
            ],
            "problemMatcher": [],
            "group": {
                "kind": "test",
                "isDefault": true
            }
        },
    ]
  1. F1唤起命令行,输入Run Task执行命令
  • 配置launch.json以在调试中运行task
json
{
      "version": "0.2.0",
      "configurations": [
          
          {
              "type": "node",
              "request": "launch",
              "name": "单元测试",
              "skipFiles": [
                  "<node_internals>/**"
              ],
              "preLaunchTask": "Run Test",
          }
      ]
  }