Cypress 之断言-语法篇

352 阅读14分钟

前言

最近在学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');
})

如何为常见用例编写断言

我们需要先模拟需要的页面,然后以此写测试用例

前端页面

image.png

image.png

前端代码

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')
})

参考资料

chai 官网

cypress官网