vue 测试

vue测试主要依赖 jest ,Vue Test Utils和Cypress,jest主要测试工具函数,Vue Test Utils是组件测试,cypress是测业务。

Jest

安装相关依赖项,

vue add @vue/unit-jest 

yarn add vue-jest babel-jest -D

ps:vscode里还需要安装jest插件


对jest.config.js文件进行相关配置,同时设置eslintrc.js

testMatch看情况自定义

module.exports = {
    preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
    transform: {
        '^.+\\\\.js$': 'babel-jest',
        '^.+\\\\.vue$': 'vue-jest'
    },
    collectCoverage: true,
    // coverageProvider: 'v8',
    moduleFileExtensions: [
        'js',
        'json',
        'jsx',
        'ts',
        'tsx',
        'node',
        'vue'
    ],
    testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'] 
}

env: {
        browser: true,
        node: true,
        es6: true,
        jest: true
  },


Jest的基本使用方法

describe与it(test)基本用法,以及一些常用的expect用法

import { isString } from '@/utils/is'

describe('isString', () => {

    it('test isString arg is defined', () => {
        expect(isString).toBeDefined()
    })

    it('test isString arg is string', () => {
        expect(isString('luanhanxiaodahaoren')).toBe(true)
    })

    // expect(..).toBeDefined() 
    // expect(..).toBe(..) 
    // expect(..).toEqual(..) 
    // expect(..).toBeInstanceOf(...)
    // expect(..).toBeNull()
})


beforeEach,afterEach与beforeAll ,afterAll的区别

在指定的作用域范围(describe或者js文件)内,beforeEach与afterEach在每个it执行之前都会运行一次,afterAll与beforeAll只会在所有的it之前和之后执行一次。


mock函数的使用

const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);

expect(mockCallback).toHaveBeenCalledTimes(2);

基本在工作主要基本结合定时器进行使用


mock对象属性的几种方法

// 1、推荐使用的,jest.spyOn() mockImplementation(),
describe('setDocumentTitle when client is Iphone', () => {
    let navigatorSpy: any
    beforeAll(() => {
        const { navigator } = window
        navigatorSpy = jest.spyOn(window, 'navigator', 'get')
        navigatorSpy.mockImplementation(() => ({ ...navigator, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) > AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D257 > MicroMessenger/6.0.1 NetType/WIFI' }))
    })

    afterAll(() => {
        navigatorSpy.mockRestore()
    })

    it('title is haha', () => {
        setTitle('haha')
        expect(document.title).toBe('haha')
    })
})

// 2、使用Object.defineProperty进行修改属性
Object.defineProperty(window, 'location', {
      get() {
        return { href: 'www.baidu.com' };
      },
    });


mock时间

mock定时器

describe('useThrottle', () => {
    beforeAll(() => {
        jest.useFakeTimers()
    })
    afterAll(() => {
        jest.useRealTimers()
    })
    afterEach(() => {
        jest.clearAllTimers()
    })
    it('should excute immediately', () => {
        const [mockFn, fn] = getHook(90, { immediate: true })
        fn()
        expect(mockFn).toHaveBeenCalledTimes(1)
        jest.advanceTimersByTime(60)
        fn()
        expect(mockFn).toHaveBeenCalledTimes(1)
    })
})

mock指定时间

import { getPastRange, setPickedTime } from '@/utils/day'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mockDate = require('mockdate')

beforeAll(() => {
    // window.Date.now = jest.fn(() => new Date('2019-04-07T10:20:30Z').getTime())
    mockDate.set('2021-01-15')
})

afterAll(() => {
    mockDate.reset()
})

describe('get the past time period', () => {
    it('get the past time period', () => {
        expect(getPastRange(0)).toEqual(['2021-01-14', '2021-01-14'])
        expect(getPastRange(6)).toEqual(['2021-01-08', '2021-01-14'])
        expect(getPastRange(29)).toEqual(['2020-12-16', '2021-01-14'])
    })

    it('set picked time ', () => {
        const mockTime = ['', '']
        setPickedTime(0, mockTime)
        expect(mockTime).toEqual(['2021-01-14', '2021-01-14'])
        setPickedTime(6, mockTime)
        expect(mockTime).toEqual(['2021-01-08', '2021-01-14'])
        setPickedTime(29, mockTime)
        expect(mockTime).toEqual(['2020-12-16', '2021-01-14'])
    })
})

mock 模块

import { openWindow } from '@/utils/utils'
jest.mock('@/utils/utils', () => ({
    openWindow: jest.fn()
}))
describe('test downloadByUrl with params', () => {
    it('test downloadByUrl with params', () => {
        downloadByUrl({ url: '/v1/admin', params: { user: 'luanhanxiao', nickname: 'dahaoren' }})
        expect((openWindow as jest.Mock).mock.calls[1][0]).toBe('')
    })
})

cypress

为什么需要?

去自动点击一个真实浏览器环境中的页面,再通过直接抓取页面上的DOM来断言是否符合预期,这是最接近用户的测试方式,所以我认为从一定程度而言端到端测试对于一个产品的发布起到了至关重要的作用

1.安装 命令

vue add @vue/e2e-cypress

2.目录结构分析 Cypress 允许配置 package.json 文件的 scripts 字段,来定义打开方式 首先,进入 Cypress安装目录 ,打开 package.json 在 scripts 下,添加

https://upload-images.jianshu.io/upload_images/19830076-bbf256d65e4f22d0.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240

在使用 cypress open 命令首次打开 Cypress,Cypress 会自动进行初始化配置并生成一个默认的文件夹结构

如果发现端口号被占用,可以杀掉进程;

image.png
https://upload-images.jianshu.io/upload_images/19830076-67209b2c0985a14c.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240
image.png

首先在启动整个Cypress时,会在项目的根目录中去寻找这个文件,

image.png

pluginsFile这个配置项指向了我们重新配置的路径,在这个被指向的配置文件中再去使用config参数配置其他目录所在的位置

image.png

当然我们也可以直接在cypress.json中去指定这些配置

(1) integration,测试文件就是测试用例 (2) plugins 插件文件,使你可以修改或扩展 Cypress 的内部行为 (3) support 每个测试文件运行之前,Cypress 都会自动加载支持文件 cypress/support/index.js eg: 只需要在 cypress/support/index.js 文件里添加 这将能实现每次测试运行前打印出所有的环境变量信息

beforeEach(() => {
    cy.log(`当前环境变量为${JSON.stringify(Cypress.env()
    )}`)
})

https://upload-images.jianshu.io/upload_images/19830076-b215d6b8ec7afbf4.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240

3.自定义cypress, 可在

cypress/support/commands.js

编写 eg:

// 全局定义获取token
Cypress.Commands.add('token', () => {
})

4.开始测试

  • 1 查找元素方式 get()
// 通过元素的 id 属性来定位
cy.get("#header").click()
// 通过元素的 class 属性来定位
cy.get(".btn").click()

  • 2 点击 click()
// 先获取 DOM 元素,再对 DOM 元素操作
// { force: true } 强制点击,和所有后续事件
cy.get('.el-date-editor').click({ force: true })

  • 3 输入内容 type()
// 在 DOM 元素中输入内容
//  先获取 DOM 元素,再对 DOM 元素进行 type 操作
 cy.get('.el-date-editor input[placeholder=结束时间]').clear().type('2021-08-29')

  • 4 清空输入框的所有内容
// 需要先拿到 DOM 元素,且是