Cypress
安装 & 运行
安装
项目目录下执行 yarn add cypress --dev
or npm install cypress --save-dev
安装
安装成功后会在项目中生成 cypress
目录和 cypress.config.js
src
+ cypress
+ e2e
+ page.cy.js // 在这里写测试脚本
+ support
+ ...
+ cypress.config.js
webpack.config.js
package.json
...
运行
cypress yarn run cypress open
自己配置到 scripts 中。
打开网页
describe('测试', () => {
it('打开视频报价系统', () => {
cy.visit('http://127.0.0.1:3000/customer-info/index', {
headers:{'myHeaderKey':'myHeaderValue'}
})
})
})
设置视口
cy.viewport(width, height) // 尺寸
cy.viewport(preset, orientation)
cy.viewport(width, height, options)
cy.viewport(preset, orientation, options)
cy.viewport(550, 750)
cy.viewport(‘iphone-6')
// 建议用此方法设置
describe('设置窗口', {
viewportHeight: 1080,
viewportWidth: 1920,
}, () => {
// test
})
选择元素
查找元素
cy.contains('type') // 通过内容选择元素 (只返回一个)
cy.get('input')
cy.get('ul li:first')
cy.get('.dropdown-menu')
cy.get('[data-test-id="test-example"]')
cy.get('a[href*="questions"]')
cy.get('[id^=local-]')
cy.get('[id$=-remote]')
cy.get('[id^=local-][id$=-remote]')
cy.get('#id\\.\\.\\.1234') // escape the character with \\
// 常用到的写法
cy.get('.el-dialog').should('not.contain', '标题不能为空') // 弹窗中不能包含 “不能为空” 字样,用来判断弹窗中的form是否校验报错
cy.get('[role="option"]').filter(':contains("跟进中")').click() // options 有多个, 从多个中过滤出指定的元素,然后点击
cy.get('[role="列表"]').each($item => { // 便利table中的所有数据,是否包含某个值
cy.wrap($item).contains('一键报价')
const dom = $item.get(0); // 获取到真实DOM
})
cy.get('.box').should('have.css', 'display', 'none') // 是否有某个css样式
元素操作
点击事件 cy.contains('登录').click()
input输入 cy.get('.input').type('fake@email.com').should('have.value', 'fake@email.com')
.click()
.dblclick()
.rightclick()
.type()
.clear()
.check()
.uncheck()
.select()
.trigger()
.selectFile()
.focus()
// 触发 hover 等事件
cy.get('.target').trigger('mousedown')
cy.get('.target').trigger('hover')
// 上传文件
cy.get('input[type=file]').selectFile('test/assets/video.mp4')
cy.get('input[type=file]').selectFile(['assets/video.mp4', 'assets/audio.mp4']) // 多文件
cy.get('input[type=file]').selectFile('test/assets/video.mp4', {force: true}) // 如果input是隐藏的
延时
cy.wait(1000) // 等待1s, 可以用来等待HTTP请求完成
// 当我们与一些元素进行交互时, 元素存在动画效果, 可以通过设置超时时间来等待动画完成后再选择元素
cy.get('.open-modal-button').click()
cy.get('#modal', {timeout: 2000}) // 等待2秒后动画结束在选择弹窗
用 should 写一个断言
详细语法 & Chai & Chai-jQuery & Sinon-Chai.
cy.url().should('include', '/customer/index') // 跳转到一个新连接, 路径包含 /customer
cy.get('.error').should('be.empty')
cy.contains('Login').should('be.visible')
cy.get('.input').type('fake@email.com').should('have.value', 'fake@email.com')
条件判断
find
获取特定选择器的子代DOM元素。用的是 jQuery 的 find 方法。
// 判断元素数量是否 >= 或者 <= 多少, 获取dom, 然后判断
cy.get('[role="case-type"] .el-cascader__tags').then( $tags => { // 获取tags 的容器
if ( $tags.find('.el-tag').length > 10) { // 通过容器获取 tag 的个数, 用来判断个数的范围. should只能判断精确的个数
// ...
}
})
示例
登录
当我们写页面测试时,需要判断登录状态。我们可以在 beforeEach
中写个自定义方法,将token等存储到 cookie 中。
// cypress.config.js
const { defineConfig } = require("cypress");
module.exports = defineConfig({
env: {
auth0_username: 'admin',
auth0_password: '123456',
}
});
// /cypress/support/commands.js
Cypress.Commands.add(
'loginByAuth0Api',
(username, password) => {
cy.log(`Logging in as ${username} ${password} `)
cy.request({
method: 'POST',
url: `http://47.107.239.240:8481/api/access/token`,
body: {
account: username,
password,
},
}).then(({ body }) => {
cy.setCookie('token', body.data.token) // 设置 cookie
})
}
)
describe('页面A', () => {
beforeEach(function () {
cy.loginByAuth0Api(
Cypress.env('auth0_username'),
Cypress.env('auth0_password')
)
})
it('Visits the Kitchen Sink', () => {
cy.getCookie('token').should('not.empty') // 看 cookie 中是否有cookie, 封装的 http 请求是从cookie 中读取token
})
})
文件导出
// 直接请求接口 跑通即可
it('文件导出', () => {
cy.getCookie('tk').then((cookie) => {
cy.request({
url: `${apiURL}/customers/export`,
headers: {
'Authorization': cookie.value.replace(/%20/, ' ') //
}
})
})
})
如果真的想要下载文件,并验证内容,点这里
完整示例
使用 Element 等框架时, 为测试的地方添加 role="" 属性, 有助于测试元素的获取
const apiURL = 'http://47.107.239.240:8481/api';
describe('登录测试', () => {
beforeEach(() => {
cy.viewport(1920, 1080)
cy.visit('/') // 因为配置了 baseUrl
cy.get('#username').type('admin') // 通过 dom 的 id 选择元素, 输入账号 admin
cy.get('#password').type('123456')
cy.contains('登录').click();
})
it('客户信息', () => {
const TRANSITION_DELAY = 400 // 动画延迟
const HTTP_DELAY = 2000 // 动画延迟
// 登录成功后, 跳转到一个新连接, 路径包含 /customer
cy.url().should('include', '/customer-info')
// table的每条数据有一个 button 按钮, 应有20条数据
cy.get('td button').should('have.length', 20)
// contains 返回一条数据(偷懒写法)
cy.get('td button').contains('编辑').click()
cy.wait(TRANSITION_DELAY); // 等待动画 transition 结束
// 出现弹窗
cy.get('.el-overlay [role="dialog"]').should('have.css', 'display', 'block');
// 修改跟进人信息
let followUp = Math.random().toFixed(6)
cy.get('.el-overlay [role="跟进人"]').focused().clear().type(`自动测试${followUp}`)
// 点击 “保存”
cy.get('button').contains('保存').click();
cy.wait(HTTP_DELAY); // 等待HTTP请求结束
// 弹窗关闭
cy.get('.el-overlay').should('have.css', 'display', 'none');
// 页面有修改过后的数据
cy.contains(`自动测试${followUp}`);
cy.clearCookies() // 清楚缓存
})
it('文件导出', () => {
cy.wait(2000);
// 从cookie中获取缓存
cy.getCookie('tk').then((cookie) => {
cy.request({
url: `${apiURL}/customers/export`,
headers: {
'Authorization': cookie.value.replace(/%20/, ' ') // decode %20
}
})
})
})
})
Cypress 组件测试
编写一个组件
// Stepper.vue
<template>
<div>
<button aria-label="decrement" @click="decrement">-</button>
<span data-cy="counter">{{ count }}</span>
<button aria-label="increment" @click="increment">+</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps(['initial'])
const emit = defineEmits(['change'])
const count = ref(props.initial || 0)
const increment = () => {
count.value++
emit('change', count.value)
}
const decrement = () => {
count.value--
emit('change', count.value)
}
</script>
加载组件
import { mount } from 'cypress/vue'
import { mount } from 'cypress/vue2' // vue2
// cypress/support/component.js
Cypress.Commands.add('mount', mount) // 这样可以再全局使用 cy.mount() , 推荐这样做
// Stepper.cy.js
import Stepper from './Stepper.vue'
describe('<Stepper>', () => {
it('mounts', () => {
cy.mount(Stepper)
})
})
npx cypress open --component
运行测试
测试 Vue 组件
测试属性
// Stepper.cy.js
import Stepper from './Stepper.vue'
const counterSelector = '[data-cy=counter]'
const incrementSelector = '[aria-label=increment]'
const decrementSelector = '[aria-label=decrement]'
it('stepper 中 count 默认应该是 0', () => {
cy.mount(Stepper)
cy.get(counterSelector).should('have.text', '0')
})
it('设置给组件传递 props', () => {
cy.mount(Stepper, { props: { initial: 100 } })
cy.get(counterSelector).should('have.text', '100')
})
测试交互
it('点击增加按钮,count应该增加', () => {
cy.mount(Stepper)
cy.get(incrementSelector).click() // 点击
cy.get(counterSelector).should('have.text', '1')
})
it('点击减少按钮,count应该减少', () => {
cy.mount(Stepper)
cy.get(decrementSelector).click()
cy.get(counterSelector).should('have.text', '-1')
})
测试组件的 Emit 事件
// <Stepper @change="onAgeChange" />
it('点击 + 触发 change 事件', () => {
const onChangeSpy = cy.spy().as('onChangeSpy') // 模拟了一个函数
cy.mount(Stepper, { props: { onChange: onChangeSpy }
cy.get(incrementSelector).click()
cy.get('@onChangeSpy').should('have.been.calledWith', 1) // 断言函数在点击后是否触发了一次
})
使用 Vue Test Utils
cy.mount(Stepper).then((wrapper) => {
// 这是 Vue Test Utils 的 wrapper
})
// 如果打算经常使用 wrapper , 可以在 cypress/support/component.js 中配置成命令
import { mount } from 'cypress/vue'
Cypress.Commands.add('mount', (...args) => {
return mount(...args).then((wrapper) => {
return cy.wrap(wrapper).as('vue')
})
})
it('2中emit测试方法', ()=>{
cy.mount(Stepper, { props: { initial: 100 } })
cy.get(incrementSelector).click()
cy.get('@vue').should((wrapper) => {
expect(wrapper.emitted('change')).to.have.length
expect(wrapper.emitted('change')[0][0]).to.equal('101')
})
// ------ spy ----------
const onChangeSpy = cy.spy().as('onChangeSpy')
cy.mount(Stepper, { props: { initial: 100, onChange: onChangeSpy } })
cy.get(incrementSelector).click()
cy.get('@onChangeSpy').should('have.been.calledWith', '101')
})
测试 slots
it('渲染弹窗内容', () => {
cy.mount(Modal, { slots: { default: () => 'Content' } })
.get(modalSelector)
.should('have.text', 'Content')
})
it('关闭弹窗', () => {
cy.mount(Modal, { slots: { default: () => 'Content' } })
.get(modalSelector)
.should('have.text', 'Content')
.get(closeButtonSelector)
.should('have.text', 'Close')
.click()
// 再次判断内容是否还存在
.get(modalSelector)
.should('not.have