Skip to main content

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

具名插槽 | 作用域插槽

配图查看更简单