前言
最近在学cypress断言,写了一些笔记,以文章形式以记之, 在闲暇之余, 温故而知新。 初次尝试写文章, 难免有不足之处,欢迎各位指出, 看到定会及时修改。
本文主要介绍cypress 基本语法, 包括了解BDD和TDD的语法风格, 学习assert 断言, expect/should 断言语法以及用断言编写一些常见的测试用例。
概念:(Assertion)是用来验证程序的运行结果是否符合预期的一种机制,它通过对实际结果和预期结果进行比较,来判断测试是否通过,Cypress 的断言基于 Chai 断言库,包括 BDD 和 TDD 格式的断言。
BDD
BDD 即 Bhavior-Driven Development, 行为驱动开发, 它关注的是系统的行为, 而不是实现的细节,它应该以多数人都容易理解的方式进行描述,BDD 风格常用的语法为exprect和should, 它们以相同的链式结构进行断言,链式调用就类似jqery或者promise那种写法。
expect/should API 覆盖BDD 断言格式。
TDD
TDD 即 Test-Driven Development,测试驱动开发,它侧重于在编写代码之前编写测试。包括编写测试,运行测试,编写通过测试的代码,它得确保代码是可测试的,并且编写的测试满足需求。这样有助于在开发周期的早期识别缺陷,减少修复缺陷的成本并提高代码质量。
TDD 风格的断言具有语言简洁、可读性强、链式调用方便等特点,可以帮助我们编写高质量的测试代码。在实际开发中,我们可以使用 TDD 风格的断言来测试常用的函数,从而保证代码的正确性和可靠性。
Assert Api 覆盖TDD 断言格式。
断言链
主要是为了提高我们断言的可读性。它本身并不具有断言的的功能,下面这些断言词, 本身其实没有意义, 把它去掉也不受到影响。
to, be,been, is,that,which, and, has.have, with,at, of,
same,but,does,still,alse。
举个栗子:
expect(foo).to.equal('bar');
expect(foo).equal('bar');
上面两个断言能够实现相同的功能。 也就是说这些词语使不使用并没有什么影响。
assert 断言
以下只介绍自己在使用过程中常用的断言词,如有错误, 欢迎指出, 如有不足,欢迎补充, 文档会及时更新。
assert 基本用法
该方法有两个参数,第一个参数是表达式,用来测试真实性,第二个参数是错误时应该显示的消息。
it.only("assrt基本用法", () => {
// 他有两个参数,
assert('foo' !== 'bar', 'foo 不等于 bar 呀');
assert(Array.isArray([]), '空数组也是一个数组呀');
})
isOk断言
第一个参数要测试的值。 第二个参数无论成功或者失败都会显示的提示信息。
it.only("assrt isOk断言", () => {
assert.isOk('everything', 'everything is ok');//在JS 中,有值的字符串就会隐式转换为 true,所以ok好吧
assert.isOk(false, 'this will fail');//FALSE 不是ok,报错
})
equal断言
断言是否相等, 相当于js 中的 == .
第一个参数是实际值
第二个参数是期望值
第三个参数是提示信息
it.only("assrt equal断言", () => {
// 非严格相等,类似与js 的 ==
// 三个参数,实际,预期,消息
assert.equal(3, '3', '这是无论成功与否都显示的消息');
})
exists断言
判断是否存在 期望的目标不是null或者undefined, 即判断为存在。 第一个参数为判断的值, 第二个参数提示消息。
it.only("assrt exists断言", () => {
// 参数与上面一样,判断是否存在
var foo = 'hi';
assert.exists(foo, 'foo 并不是`null` 或者 `undefined`,所以是存在的');
})
include 断言
include 可用于断言是否包含某个值, 可用于判断数组中是否包含某个值,字符串中的是否有子字符串或者对象的中是否包含其属性子集
it.only("assrt include 断言", () => {
// 三个参数,实际,预期,消息
// 可用于字符串,数组,对象
assert.include([1, 2, 3], 2, '数组中有2值');
assert.include('foobar', 'foo', 'foobar 中有foo');
assert.include({ foo: 'bar', hello: 'universe' }, { foo: 'bar' }, '对象中包含 foo: bar');
})
注意: 该方法是严格相等(===)
当断言某个数组的是否包含某个值时, 会搜索数组中的值与给定值严格相当的元素
当断言对象中的某个属性子集时, 会搜索对象查找给定的属性值,检查对象的每个键是否存在并且与给定的属性值严格相等。
match
通过正则表达式断言是否符合要求。 第一个参数是需要断言的值。 第二个参数是 正则表达式 第三个提示信息
it.only("assrt match", () => {
assert.match('foobar', /^foo/, 'foobar能够匹配到foo')
})
lengthOf
断言该对象的长度
it("assrt lengthOf", () => {
// 三个参数,实际,预期,消息
assert.lengthOf([1, 2, 3], 3, '[1, 2, 3] 数组长度为3');
assert.lengthOf('foobar', 6, 'foobar 长度为6');
})
isEmpty
判断是否为空
it.only("assrt isEmpty", () => {
// 下面都是空
assert.isEmpty([]);
assert.isEmpty('');
assert.isEmpty({});
})
expect/should 断言
以下只介绍自己在使用过程中常用的断言词,如有错误, 欢迎指出, 如有不足,欢迎补充, 文档会及时更新。
const errorMessage = '我是一个错误的消息呀,看见我说明出错啦!'
not 断言
否定链中后续的所有断言
it(".not 断言", () => {
expect({ a: 1 }).to.not.have.property('b');//这个对象中并没有属性b
expect([1, 2]).to.be.an('array').that.does.not.include(3);//这是一个数组但是它并没有包含我们的元素3,只有1和2
// 我们可以通过.not否定后续的所有断言,但是我们一般不建议这样做
// 因为我们相当于否定了无数意外的结果, 我们最好应该有一个预期的结果
expect(2).to.equal(2); // 推荐写法,2 只能等于2 ,只有一个预期的结果
expect(2).to.not.equal(1); // 不推荐写法,2 不等于1,但是它也可能不等于3,4,5,6等等无数种可能
})
include
我们期望的结果中是否包含有某些指定的值, include 即包含。
常见的有我们的字符串,数组和对象,
当然也可以是Set,WeakSet,Map 等数据结构
it(".include断言", () => {
expect('foobar').to.include('foo'); //foobar 字符串种包含foo呀
expect([1, 2, 3]).to.include(2); //[1, 2, 3] 数组 有2 呀
expect({ a: 1, b: 2, c: 3 }).to.include({ a: 1, b: 2 }); //{ a: 1, b: 2, c: 3 }中包含我们的a: 1, b: 2呀
// 先判断类型,在判断包含的值
expect([1, 2, 3]).to.be.an('array').that.includes(2);// an 判断是一个数组,在又包含我们的2
})
equal
判断两值是否等于
it("equal 断言", () => {
const errorMessage = '我是一个错误的消息呀,看见我说明出错啦!'
// 1.常用写法
expect(1).to.equal(1);//1 等于1
expect('foo').to.equal('foo'); //foo 等于foo
expect(1).to.not.equal(2); // 不推荐写法
// 2、错误提示, 可以接受错误信息,即断言失败时显示的自定义错误消息。该消息也可以作为第二个参数给出expect。
expect(1).to.equal(2, errorMessage);// 1 不等于2 所以出错啦
expect(1, errorMessage).to.equal(2);// 1 不等于2 所以出错啦
})
exist
判断是否存在
断言的目标不严格等于null 或者undefined,即判断为存在。
it("exist断言", () => {
// 1、断言的目标存在,不严格等于 nuLL 和undefined
const errorMessage = '我是一个错误的消息呀,看见我说明出错啦!'
expect(1).to.equal(1); // 期望1 是1
expect(1).to.exist; // 期望1 是存在的
expect(0).to.equal(0); // 期望0 是0
expect(0).to.exist; // 期望0 是存在的
///2、可以和not 配和使用,判断一个值不存在
expect(null).to.be.null; // 期望null 是null
expect(null).to.not.exist; // 期望null并不存在
expect(undefined).to.be.undefined; // 期望 undefined 未定义 是 undefined
expect(undefined).to.not.exist; // 期望undefined(未定义)并不存在
// 3、可以自定义错误消息作为第二个参数
expect(null, errorMessage).to.exist;
})
empty
判断是否为空 当判断是字符串或者数组,empty 会断言是否严格相等于0
it("empty断言", () => {
// 1、空的,严格不等于,常用于断言字符串,数组,对象。
const errorMessage = '我是一个错误的消息呀,看见我说明出错啦!'
expect([]).to.be.empty;// 数组是个为空
expect('').to.be.empty;// 字符串为空
expect({}).to.be.empty;// 空对象
// 2、因为根据目标类型执行不同的操作,所以在使用之前检查目标的类型很重要
expect([]).to.be.an('array').that.is.empty;// 先判断是什么类型,在判断是否为空
// 3、可以自定义错误消息作为第二个参数
expect([1, 2, 3], errorMessage).to.be.empty;// 数组不为空所以报错啦
})
within
within 在......之内
断言目标是数字或者日期时,当大于等于给定的数字或者高于日期的开始,或者小于等于给定的数字或 者小于等于日期的结束, 但是一般情况下断言目标应该等于我们的预期值。
it("within", () => {
// 断言目标是一个大于或等于给定数字或日期的数字或日期start,以及小于或等于给定数字或日期的数字或日期finish
// 在一段值之内,可以理解为 1<x<3, X在1和3之内
const errorMessage = '我是一个错误的消息呀,看见我说明出错啦!'
// 1、基本判断。
expect(2).to.equal(2); // 推荐2 等于2
expect(2).to.be.within(1, 3); // 2 在1 和 3 之间
expect(2).to.be.within(2, 3); // 2<=2<=3
expect(2).to.be.within(1, 2); // 1<=2<=2
// 2.在链中较早的加以判断
expect(1).to.equal(1); // 1 等于1
expect(1).to.not.be.within(2, 4); // 1不在 2和4之间
// 错误提示
expect(4).to.be.within(1, 3, errorMessage);// 报错,4不在1和3之间会有提示
expect(4, errorMessage).to.be.within(1, 3); // 报错,4不在1和3之间会有提示
})
throw
判断是否有错误
it("throw", () => {
// 当没有提供参数时,调用目标函数并断言引发错误。.throw
// 调用该函数就会报错,我们预判该错误,所以会正常执行
var badFn = function () { throw new TypeError('Illegal salmon!'); };
expect(badFn()).to.throw();
})
members
断言目标数组具有与给定数组相同的成员。
it(".members", () => {
expect([1, 2, 3]).to.have.members([2, 1, 3]);// 成员相同 1,2,3
expect([1, 2, 2]).to.have.members([2, 1, 2]);// 成员相同 1,2
// 默认情况下,顺序无关紧要。在链中添加得更早,以要求成员以相同的顺序出现。
expect([1, 2, 3]).to.have.ordered.members([1, 2, 3]);
expect([1, 2, 3]).to.have.members([2, 1, 3]).but.not.ordered.members([2, 1, 3]);
// 默认情况下,两个数组的大小必须相同。在链中添加较早的元素以要求目标成员是预期成员的超集。
// 注意一哈, 添加时子集中的重复项将被忽略。
expect([1, 2, 3]).to.include.members([1, 2]);//
expect([1, 2, 3]).to.not.have.members([1, 2]);
// 期待我们的成员有1,2,3,虽然2重复了3次,但是它仍然被包含在预期成员中。
expect([1, 2, 3]).to.include.members([1, 2, 2, 2]);
expect([1, 2]).to.have.members([1, 2, 3], errorMessage);// 多了个3,所以报错啦
expect([1, 2], errorMessage).to.have.members([1, 2, 3]);
})
any
使后续链中至少有一个给定的值
it(".any 断言", () => {
// 使链要求目标至少有一个给定的键
expect({ a: 1, b: 2 }).to.have.any.keys('b');// 对象中至少有一个属性b
})
all
使后续链中的所有目标都要求是给定的值
it(".all断言", () => {
// 使链中后续的所有断言都要求目标具有所有给定的键
expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b');// 对象中满足给定键a 和b
})
ok
断言目标值是一个真值(在布尔类型上下文中即为true)
it("ok断言", () => {
// 判断是一个真值即为 true
expect(1).to.equal(1); // 1等于1 啦
expect(1).to.be.ok; // 1 不推荐写法啊,1会隐式转换为true
expect(true).to.be.true; // 就是相等,不用解释
expect(true).to.be.ok; // OK 以为 true,不推荐写法
})
match
断言目标与给定的正则表达式匹配
it(".match", () => {
// 断言目标与给定的正则表达式匹配
expect('foobar').to.match(/^foo/);
})
lengthOf
断言目标的长度
it.only("lengthOf", () => {
// 断言目标的长度
expect([1, 2, 3]).to.have.lengthOf(3); //3 个值等于3
expect('foo').to.have.lengthOf(3);
// 3.用于判断在某种范围,和.above, .below, .least, .most, 是使用判断范围
// above 在上面
// below 在下面
// least 至少
// most 最多
expect([1, 2, 3]).to.have.lengthOf(3);// 3个值等于3
expect([1, 2, 3]).to.have.lengthOf.above(2);// 3个值大于2
expect([1, 2, 3]).to.have.lengthOf.below(4); // 3个值小于4
expect([1, 2, 3]).to.have.lengthOf.at.least(3);// 3个值大于等于3
expect([1, 2, 3]).to.have.lengthOf.at.most(3);// 3个值小于等于3
expect([1, 2, 3]).to.have.lengthOf.within(2, 4);// 3个值在2到4之间
// 3.错误信息
expect([1, 2, 3]).to.have.lengthOf(2, errorMessage);
expect([1, 2, 3], errorMessage).to.have.lengthOf(2);
})
an
an的别名是a ,可以互换,用于判断目标的类型
it(".an", () => {
// an的别名是a ,两个可以互换的使用。
// 1.断言目标的类型
expect('foo').to.be.a('string');
expect({ a: 1 }).to.be.an('object');
expect(null).to.be.a('null');
expect(undefined).to.be.an('undefined');
expect(new Error).to.be.an('error');
// 2. 也可以用作语言链来提高断言的可读性。
expect({ b: 2 }).to.have.a.property('b');
})
如何为常见用例编写断言
我们需要先模拟需要的页面,然后以此写测试用例
前端页面
前端代码
home.js
import React, { memo } from 'react'
import { Button, Divider, Spin, Radio } from 'antd'
import CustomForm from './form'
const Home = memo(() => {
return (
<div>
<div
data-testid="todo"
>
<h5>我是list</h5>
<ul>
<li>111</li>
<li>222</li>
<li>我是需要匹配的li文本</li>
<li className='hidden' style={{
display: 'none'
}}> 页面看不见我看不见我,我是偷偷隐藏的li文本</li>
</ul>
</div>
<Divider />
<div>
<h5>我是form表单</h5>
<CustomForm />
</div>
<Divider />
<div data-testid="test-text">
<span>我是用来测试的文本呀呀</span>
<span></span>
</div>
<Divider />
<div data-testid="hello-text">
<span>Hello,很高兴你对编写cypress感兴趣</span>
<span></span>
</div>
<Divider />
<Spin
tip="Loading"
size="small"
visible={true}
data-testid="loading"
>
我是要显示的页面
</Spin>
<Divider />
<Radio>我是一个小小的单选框,用来测试,看见我请快点选中,不然会报错哦!</Radio>;
<Divider />
<div>
<p
data-testid="decorationId"
style={{
textDecoration: "line-through solid rgb(0, 0, 0)",
}}
>
我是一个有删除线的文本呀
</p>
</div>
<Divider />
<div>
<Button data-testid="example-input" disabled={true}>
我是一个需要测试的button
</Button>
<span data-testid="todo-item" className='completed'> 我是没啥存在感的路人甲文本</span>
<span data-testid="todo-item" className='completed'> 我是没啥存在感的路人乙文本</span>
<span> 我是没啥存在感的路人丙文本</span>
</div>
<Divider />
<div>
<span
className='testAttr'
bar="bar"
foo="123"
id='testAttr'
>我是chai-jquery的测试文本</span>
</div>
</div>
)
})
export default Home
form.js
import React from 'react';
import { Button, Checkbox, Form, Input } from 'antd';
const onFinish = (values) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo);
};
const CustomForm = () => (
<Form
name="basic"
labelCol={{
span: 8,
}}
wrapperCol={{
span: 16,
}}
style={{
maxWidth: 300,
}}
initialValues={{
remember: true,
username: "cxw"
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label="用户名"
name="username"
rules={[
{
required: true,
message: 'Please input your username!',
},
]}
>
<Input data-testid={"user-name"} />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="我是文本框"
name="password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item name="remember" valuePropName="checked" label={null}>
<Checkbox>记住我</Checkbox>
</Form.Item>
<Form.Item label={null}>
<Button
data-testid="form-submit"
type="primary"
htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
);
export default CustomForm;
测试用例
判断列表长度值是否为4
it("判断列表长度值是否为4", () => {
cy.get('li').should('have.length', 4)
})
判断form表单input的class并没有disabled名
it("判断form表单input的class并没有disabled名", () => {
cy.get('form').find('input').should('not.have.class', 'disabled')
})
获取文本框的value
it("获取文本框的value", () => {
cy.wait(2000)
// 输入框的值为cxw
cy.get('[data-testid="user-name"]').should('have.value', 'cxw')
cy.get('[data-testid="test-text"]').should('include.text', '我是用来测试的文本呀呀')
//获取当前元素,匹配它的文本以Hello 开头
cy.get('[data-testid="hello-text"]')
.invoke('text')
.should('match', /^Hello/)
//获取当前元素,匹配它的文本包含hello
cy.contains('[data-testid="hello-text"]', /Hello/)
})
判断是否显示
it("判断是否显示", () => {
// 提交按钮是否显示
cy.get('[data-testid="form-submit"]').should('be.visible')
// li 元素是否显示
cy.contains('[data-testid="todo"] li', '我是需要匹配的li文本').should('be.visible')
// li 元素是否显示
cy.get('li').should('be.visible')
// li 元素是隐藏
cy.get('li.hidden').should('not.be.visible')
})
判断是否在加载中
it("判断是否在加载中", () => {
// 一般情况下,我们是判断它是为not.exist ,不存在的,如果一直在加载可能就是后端没返数据,就一直显示加载中
// 即为cy.get('[data-testid="loading"]').should('not.exist')
// 这里为了页面显示,就先加载中吧!!!
cy.get('[data-testid="loading"]').should('exist')
})
判断单选框是否选中
it("判断单选框是否选中", () => {
cy.get(':radio').should('be.not.checked')
})
判断是否有对应的css属性
it("判断是否有对应的css属性", () => {
// 判断这个元素的css属性是否为line-through solid rgb(0, 0, 0)
// 注意,这里使用的.CSS 选择器是 chai-jquery的语法,需要下载对应库
cy.get('[data-testid="decorationId"]').should(
'have.css',
'text-decoration',
'line-through solid rgb(0, 0, 0)'
)
// 这个元素没有disPlay:none
cy.get('[data-testid="decorationId"]').should('not.have.css', 'display', 'none')
})
判断是否是禁止按钮。然后将其改为可点击
it("判断是否是禁止按钮。然后将其改为可点击", () => {
// 先获取该元素,检测该元素是否处于禁用状态,如果处于禁用状态,则执行后面的代码
// .invoke('prop', 'disabled', false) 的作用是将选中的元素的 disabled 属性设置为 false,
// 即取消该元素的禁用状态,使其变为可交互状态即将disabled=true 改为disabled = false
cy.get('[data-testid="example-input"]')
.should('be.disabled')
.invoke('prop', 'disabled', false)
// 判断该元素是否处于启用状态,他的disabled 属性为false
cy.get('[data-testid="example-input"]')
.should('be.enabled')
.and('not.be.disabled')
})
肯定断言
it("肯定断言", () => {
// 这个元素的数量为2,并且有completed 这个class
cy.get('[data-testid="todo-item"]')
.should('have.length', 2)
.and('have.class', 'completed')
// contains 命令是查找包含的文本断言 。 在该句中是查找包含文本 '我是没啥存在感的路人丙文本' 的 DOM 元素。
// 后面是查找不包含类 completed 的 DOM 元素。
// 所以这句话的意思是 查找包含文本 '我是没啥存在感的路人丙文本' 的但是不包含类 completed 的 DOM 元素。
cy.contains('我是没啥存在感的路人丙文本').should('not.have.class', 'completed')
})
否定断言-错误通过
it("否定断言-错误通过", () => {
// 断言 form 表单下的input 没有disabled这个class类
cy.get('form').find('input').should('not.have.class', 'disabled')
})