jest @vue/unit-jest入门学习

178 阅读4分钟

1.安装

$ npm i jest (全局安装)
$ npm init -y (创建package.json)
$ npm install -D  babel-jest babel-core babel-preset-env regenerator-runtime (让node项目支持import)

2.使用

  1. jest 指定文件夹 (会运行下面所有xx.test.js 的文件)
  2. 创建项目文件夹下的__test__和对应下面的.test.js只是用于规范命名,不是强制需要,除非设置如下:
//通过设置 package.json 的 可以指定约束测试文件夹
"jest": 
{
 "testMatch": [
  "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
  "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
  ],
}

2.1 或者直接创建jest.config.js

// jest.config.js 
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['./tests'],
  testRegex: '.*\\.test\\.ts$',
  moduleFileExtensions: ['ts', 'js', 'json', 'node'],
} 
  1. 执行命令 npm run jest
  "scripts": { 
    "jest": "jest"
  },

3.变量期望

expect(XXX).toBe(YYY)

// functions.js
export default {
  sum(a, b) {
    return a + b;
  }
}

// functions.test.js
import functions  from './sum.js';

test('sum(2 + 2) 等于 4', () => {
  expect(functions.sum(2, 2)).toBe(4);
})

4.方法期望

1.jest.fn() 普通方法断言

test('测试jest.fn()返回固定值', () => {
    let mockFn = jest.fn().mockReturnValue('default');
    // 断言mockFn执行后返回值为default
    let a = mockFn();
    expect(mockFn).toBeCalledTimes(1);
    expect(a).toBe('default');
  })
  
  test('测试jest.fn()内部实现', () => {
      let testFn = (num1,num2) => {
          return num1 * num2;
      }
    let mockFn = jest.fn( testFn );
    // 断言mockFn执行后返回100
    expect(mockFn(20, 10)).toBe(200);
  }) 

2.通过done参数延迟测试的时间

处理settimeout定时器的用例,默认定时回调任务是不会只想,当作测试成功处理。

    test('测试xxx', done => {
        func()//测试带有延迟的方法 
        // 延时2s结束
        setTimeout(done, 2000)
    })

3.expect.assertions 实现异步断言

// functions.js 记得先安装 npm i axios
import axios from 'axios';

export default {
   fetchUser() {
    return axios.get('http://jsonplaceholder.typicode.com/users/1')
      .then(res => res.data)
      .catch(error => console.log(error));
  }
}

import functions  from './functions';

test('开始异步断言', () => {
  expect.assertions(1);
  return functions.fetchUser()
    .then(data => {
      expect(data.name).toBe('Leanne Graham');
    });
})  

4.测试调用次数与参数

//测试订阅发布模式
test('开始异步断言', () => {
    const mockFn = jest.fn()
    const Connention = require('../index')//这里为订阅发布的代码
    const conn = new Conection();
    //订阅两次
    conn.onConn(mockFn)
    conn.onConn(mockFn)

    setTimeout(() => {
        conn.contention("连接1完成")
    })
    setTimeout(() => {
        conn.contention("连接2完成")
    })
    
    setTimeout(() => {
        const calls = mockFn.mock.calls
        expect(calls.length).toBe(4)
        expect(calls.[0][0]).toBe('连接1完成')
        expect(calls.[1][0]).toBe('连接1完成') //第一个[1] 为调用的第一个参数,第二个[0]为第二个参数
        expect(calls.[2][0]).toBe('连接2完成')
        expect(calls.[3][0]).toBe('连接2完成')
    }) 
    
})

jest每次限制5秒的时间

    
test('开始异步断言', (done) => {

       let testFn = (num1,num2) => {
          return num1 * num2;
          }
        let mockFn = jest.fn( testFn );
        // 断言mockFn执行后返回100 

    const mockFn = jest.fn() 
    setTimeout(() => {
         expect(mockFn(20, 10)).toBe(200);
         
    },6000)  //会提示超时
})

这是因为Jest每个测试用例默认只给了5s。

jest.useFakeTimers()//使用jest 自己的模拟的定时器
jest.advanceTimersByTime(6000); //把时间往后拨6秒
jest.runAllTimers() //将全部定时器立即运行结束
jest.runOnlyPendingTimers() //当有多个定时器 如果内部有多个定时器,只想运行一个定时器
jest.useRealTimers()//切换为js正常时间

//使用setImmediate 实现微队列执行
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
 it('测试一段时间之后自动关闭', async () => {
 jest.useFakeTimers() //使用jest的定时器
  setTimeout(() => {
      //....
  },6000)
 
 jest.runOnlyPendingTimers()//跳过当前第一个settimeout
 await flushPromises()
  expect(xxxx).toBe(false)
 
})

完整的用法

const flushPromises = () => new Promise(resolve => setImmediate(resolve));
 it('测试一段时间之后自动关闭', async () => {
    jest.useFakeTimers() //使用jest的定时器
    const wrapper = mount(Notification, {
      props: {
        duration: 8000
      }
    })
    
    jest.runOnlyPendingTimers()//跳过当前第一个settimeout
    await flushPromises()
    expect(wrapper.get('.el-notification').isVisible()).toBe(false)
     })

vue 自动化测试

vue add @vue/unit-jest 
// counter.js

export default {
  template: `
    <div>
      <span class="count">{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `,

  data() {
    return {
      count: 0
    }
  },

  methods: {
    increment() {
      this.count++
    }
  }
}

挂载组件

// test.js

// 从测试实用工具集中导入 `mount()` 方法
// 同时导入你要测试的组件
import { mount } from '@vue/test-utils'
import Counter from './counter'

// 现在挂载组件,你便得到了这个包裹器
const wrapper = mount(Counter)

// 你可以通过 `wrapper.vm` 访问实际的 Vue 实例
const vm = wrapper.vm

// 在控制台将其记录下来即可深度审阅包裹器
// 我们对 Vue Test Utils 的探索也由此开始
console.log(wrapper)

测试组件渲染出来的 HTML

import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Counter', () => {
  // 现在挂载组件,你便得到了这个包裹器
  const wrapper = mount(Counter)

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // 也便于检查已存在的元素
  it('has a button', () => {
    expect(wrapper.contains('button')).toBe(true)
  })
})

模拟用户交互

it('button click should increment the count', () => {
  expect(wrapper.vm.count).toBe(0)
  const button = wrapper.find('button')
  button.trigger('click')
  expect(wrapper.vm.count).toBe(1)
})

例子-通知组件

Notification.vue

<template>
  <div v-show="visible" :class="['el-notification',verticalProperty,horizontalClass]" :style="positionStyle"
    @click="onClickHandler">
    <div class="el-notification__title">
      {{ title }}
    </div>
    <div class="el-notification__content">
      {{ message }}
    </div>
    <button v-if="showClose" class="el-notification__close-button" @click="onCloseHandler"></button>
  </div>
</template>
<script setup lang="ts">
import { getCurrentInstance, ref, computed, withDefaults } from 'vue'
const instance = getCurrentInstance()
const visible = ref(true)

interface Props {
  title: string,
  message: string,
  verticalOffset: number,
  type: string,
  position: string,
  showClose: boolean,
  duration:number
}
const props = withDefaults(defineProps<Props>(), {
  title: '',
  message: '',
  verticalOffset: 0,
  type: '',
  position: 'top-right',
  showClose: true,
  duration:3
})
const verticalOffsetVal = ref(props.verticalOffset)
const typeClass = computed(() => {
  return props.type ? `el-icon-${props.type}` : ''
})
const horizontalClass = computed(() => {
  return props.position.endsWith('right') ? 'right' : 'left'
})
const verticalProperty = computed(() => {
  return props.position.startsWith('top') ? 'top' : 'bottom'
})
const positionStyle = computed(() => {
  return {
    [verticalProperty.value]: `${verticalOffsetVal.value}px`
  }
})
function onCloseHandler() {
  visible.value = false
}

let timer
function delayClose() {
  if (props.duration > 0) {
    timer = setTimeout(() => {
      onCloseHandler()
    }, props.duration)
  }
}
delayClose()
</script>
<style lang="scss">
.el-notification {
  position: fixed;
  right: 10px;
  top: 50px;
  width: 330px;
  padding: 14px 26px 14px 13px;
  border-radius: 8px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  overflow: hidden;
}
</style>

Notification.spec.ts


import Notification from './Notification.vue';
import { mount } from '@vue/test-utils';
  
describe("Notification", () => { 
  
  it('测试标题title', () => {
    const title = 'this is a title'
    const wrapper = mount(Notification, {
      props: {
        title
      }
    })
    expect(wrapper.get('.el-notification__title').text()).toContain(title)
  })
  it('测试内容', () => {
    const message = 'this is a message'
    const wrapper = mount(Notification, {
      props: {
        message
      }
    })
    expect(wrapper.get('.el-notification__content').text()).toContain(message)
  })
  it('测试渲染位置', () => {
    const position = 'bottom-right'
    const wrapper = mount(Notification, {
      props: {
        position
      }
    })
    expect(wrapper.find('.el-notification').classes()).toContain('right')
    expect(wrapper.vm.verticalProperty).toBe('bottom')
    const el = <HTMLElement>wrapper.find('.el-notification').element
    expect(el.style.bottom).toBe('0px')
  })
  it('测试位置偏移', () => {
    const verticalOffset = 50
    const wrapper = mount(Notification, {
      props: {
        verticalOffset
      }
    })
    expect(wrapper.vm.verticalProperty).toBe('top') 
    const el = <HTMLElement>wrapper.find('.el-notification').element
    expect(el.style.top).toBe( `${verticalOffset}px` )
  })


  it('测试按钮参数', () => {
    const showClose = false
    const wrapper = mount(Notification, {
      props: {
        showClose
      }
    })
    expect(wrapper.find('.el-notification__close-button').exists()).toBe(showClose)
    // expect(wrapper.find('.el-icon-close').exists()).toBe(true)
  })
  it('测试点击关闭按钮', async () => {
    const showClose = true
    const wrapper = mount(Notification, {
      props: {
        showClose
      }
    })
    const closeBtn = wrapper.get('.el-notification__close-button')
    await closeBtn.trigger('click')
    expect(wrapper.get('.el-notification').isVisible()).toBe(false)
  })
  it('测试一段时间之后自动关闭', async () => {
    jest.useFakeTimers()
    const wrapper = mount(Notification, {
      props: {
        duration: 8000
      }
    })
    const flushPromises = () => new Promise(resolve => setImmediate(resolve));
    jest.runOnlyPendingTimers() 
    await flushPromises()
    expect(wrapper.get('.el-notification').isVisible()).toBe(false)
     })

})