4 测试用例编写和组织
4.1 用例结构
Cypress是建立在 Mocha 和 Chai 之上,因此同时支持Chai的 BDD 和 TDD 两种风格。如果你熟悉JavaScript风格的代码,那么在Cypress中写测试用例是很容易上手的。
Mocha是一款适用于Node.js和浏览器的测试框架,可使用异步测试变得简单灵活。
Cypress的测试风格继承于Mocha,提供了 describe() 、 context() 、 it() 、 specify() 四个关键字,对于一条可执行的测试而言,必须包含以下两个组成部分:
- describe()和context()等效,均表示一个测试套件或测试集
- it()和specify()等效,均表示一个测试用例
示例如下所示:
describe('我是一个测试集', () => {
it('测试用例-1', () => {
expect(1+2).to.eq(3)
});
it('测试用例-2', () => {
expect(3-2).to.eq(1)
});
it('测试用例-3', () => {
expect(3*2).to.eq(5)
});
});
最终的运行结果如下所示:
4.2 Hook
Hook中文常翻译为 钩子函数 ,Cypress也提供了Hook(从Mocha继承而来)。这些Hook函数能够在运行每个测试用例或测试集之前,做一些准备操作,也可以每个测试用例或测试集运行完成之后执行一些操作。示例如下所示:
///
before(()=>{
// 全局Hook
// 所有用例运行行前执行,但仅执行一次
cy.log("我是全局before Hook,所有测试用例运行前执行我,但仅执行一次")
});
beforeEach(()=>{
// 全局Hook
// 每个测试用例运行前执行
cy.log("我是全局beforeEach Hook,每个测试用例运行前执行我")
});
afterEach(()=>{
// 全局Hook
// 每个测试用例运行完成后执行
cy.log("我是全局afterEach Hook,每个测试用例运行完成后执行我")
});
after(()=>{
// 全局Hook
// 所有测试用例运行完成后执行,但仅执行一次
cy.log("我是全局after Hook,所有测试用例运行完成后执行,但仅执行一次")
});
describe('Test Hooks in TestSuite', () => {
before(()=>{
// 当前测试集Hook
// 当前测试集中,所有测试用例运行前执行,但仅执行一次
cy.log("我是当前测试集before Hook,所有测试用例运行前执行我,但仅执行一次")
});
beforeEach(()=>{
// 当前测试集Hook
// 当前测试集中,每个测试用例运行前执行
cy.log("我是当前测试集beforeEach Hook,每个测试用例运行前执行我")
});
afterEach(()=>{
// 当前测试集Hook
// 当前测试集中,每个测试用例运行完成后执行
cy.log("我是当前测试集afterEach Hook,每个测试用例运行完成后执行我")
});
after(()=>{
// 当前测试集Hook
// 当前测试集中,所有测试用例运行完成后执行,但仅执行一次
cy.log("我是当前测试集after Hook,所有测试用例运行完成后执行,但仅执行一次")
});
it('Test Hook in case-1', () => {
cy.log("我是测试用例-1");
cy.visit("https://www.baidu.com/",{timeout:10000});
});
it('Test Hook in case-2', () => {
cy.log("我是测试用例-2");
cy.visit("https://www.baidu.com/",{timeout:10000});
});
});
最终的运行结果如下所示:
从以上示例代码可以总结出来运行顺序如下所示:
- 仅在测试开始时运行 before 且仅运行一次
- 任何一个用例运行前都要运行 beforeEach
- 运行测试用例
- 任何一个用例结束时都要运行 afterEach
- 仅在测试结束时运行 after 且仅运行一次
4.2.1 before()/after()
before()是所有测试用例的 统一前置动作 ,而before()在一个describe()内只会执行一次,其执行顺序在所有测试用例it()之前。after()是所有测试用例的 统一后置动作 ,而after()在一个describe()内只会执行一次,其执行顺序在所有测试用例it()之后。示例代码如下所示:
///
describe('', () => {
let baseUrl="https://example.cypress.io/todo";
before(()=>{
cy.log("所有用例运行前执行before,仅执行一次");
});
it('测试用例-1', () => {
cy.visit(baseUrl,{timeout:10000});
cy.get(".header input").should("have.class","new-todo");
});
it('测试用例-2', () => {
cy.visit(baseUrl,{timeout:10000});
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
after(()=>{
cy.log("所有用例运行完成后执行after,仅执行一次");
});
});
最终的运行结果如下所示:
4.2.2 beforeEach()/afterEach()
beforeEach()是每个测试用例执行前的 前置操作 ,即每个用例执行前都会执行一次。一个describe()有几个用例it()就会执行几次。afterEach()是每个测试用例执行后的 后置操作 ,即每个用例执行完成后都会执行一次。一个describe()有几个用例it()就会执行几次。示例代码如下所示:
///
describe('', () => {
let baseUrl="https://example.cypress.io/todo";
before(()=>{
cy.log("所有用例运行前执行before,仅执行一次");
});
beforeEach(()=>{
cy.log("每个用例执行前,均会执行一次beforeEach");
cy.visit(baseUrl,{timeout:10000});
});
it('测试用例-1', () => {
cy.get(".header input").should("have.class","new-todo");
});
it('测试用例-2', () => {
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
afterEach(()=>{
cy.log("每个用例执行完成后,均会执行afterEach");
});
after(()=>{
cy.log("所有用例运行完成后执行after,仅执行一次");
});
});
最终的运行结果如下所示:
4.3 包含/排队用例
通过学习Hook,我们可以把测试的前置和后置条件进行剥离,更好的编写和组织测试用例。在实际项目中,我们也需要仅运行指定的测试用例或跳过某些用例等。Cypress也提供相应的功能,一起来看看吧。
4.3.1 包含测试集/测试用例
在Cypress提供该功能的是 .only() 。当使用.only()指定某个测试集/测试用例后,仅当指定了的测试集/测试用例才会运行,其他未指定的测试集/测试用例则不会运行。示例如下所示:
- 1.仅运行指定测试集
///
describe.only('包含only', () => {
let baseUrl="https://example.cypress.io/todo";
before(()=>{
cy.log("所有用例运行前执行before,仅执行一次");
});
beforeEach(()=>{
cy.log("每个用例执行前,均会执行一次beforeEach");
cy.visit(baseUrl,{timeout:20000});
});
it('测试用例-1', () => {
cy.get(".header input").should("have.class","new-todo");
});
it('测试用例-2', () => {
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
afterEach(()=>{
cy.log("每个用例执行完成后,均会执行afterEach");
});
after(()=>{
cy.log("所有用例运行完成后执行after,仅执行一次");
});
});
describe('不包含only', () => {
let tempUrl="https://www.baidu.com";
beforeEach(()=>{
cy.visit(tempUrl);
});
it('不包含only测试用例-1', () => {
cy.get("#kw").type("Surpass{Enter}");
// cy.get("#kw").type("Surpass");
// cy.get("#su").click();
});
afterEach(()=>{
cy.log("每个用例执行完成后,均会执行afterEach");
});
});
最终的运行结果如下所示:
- 2.仅运行指定测试用例
describe.only('包含only', () => {
let baseUrl="https://example.cypress.io/todo";
before(()=>{
cy.log("所有用例运行前执行before,仅执行一次");
});
beforeEach(()=>{
cy.log("每个用例执行前,均会执行一次beforeEach");
cy.visit(baseUrl,{timeout:20000});
});
it('测试用例-1', () => {
cy.get(".header input").should("have.class","new-todo");
});
it.only('测试用例-2', () => {
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
afterEach(()=>{
cy.log("每个用例执行完成后,均会执行afterEach");
});
after(()=>{
cy.log("所有用例运行完成后执行after,仅执行一次");
});
});
最终的运行结果如下所示:
4.3.2 排除测试集/测试用例
在Cypress提供该功能的是 .skip() 。当使用.skip()指定某个测试集/测试用例后,则不会运行该测试集/测试用例,其他未指定的测试集/测试用例则会运行。示例如下所示:
- 1.排除指定测试集
///
describe('包含only', () => {
let baseUrl="https://example.cypress.io/todo";
before(()=>{
cy.log("所有用例运行前执行before,仅执行一次");
});
beforeEach(()=>{
cy.log("每个用例执行前,均会执行一次beforeEach");
cy.visit(baseUrl,{timeout:20000});
});
it('测试用例-1', () => {
cy.get(".header input").should("have.class","new-todo");
});
it.only('测试用例-2', () => {
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
afterEach(()=>{
cy.log("每个用例执行完成后,均会执行afterEach");
});
after(()=>{
cy.log("所有用例运行完成后执行after,仅执行一次");
});
});
describe.skip('不包含only', () => {
let tempUrl="https://www.baidu.com";
beforeEach(()=>{
cy.visit(tempUrl);
});
it('不包含only测试用例-1', () => {
cy.get("#kw").type("Surpass{Enter}");
// cy.get("#kw").type("Surpass");
// cy.get("#su").click();
});
afterEach(()=>{
cy.log("每个用例执行完成后,均会执行afterEach");
});
});
最终的运行结果如下所示:
- 2.排除指定测试用例
describe('顶层测试集', () => {
let baseUrl="https://example.cypress.io/todo";
before(()=>{
cy.log("所有用例运行前执行before,仅执行一次");
});
beforeEach(()=>{
cy.log("每个用例执行前,均会执行一次beforeEach");
cy.visit(baseUrl,{timeout:20000});
});
it('测试用例-1', () => {
cy.get(".header input").should("have.class","new-todo");
});
it.skip('测试用例-2', () => {
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
describe.skip('嵌套测试集', () => {
it('嵌套测试集用例-1', () => {
cy.log("测试嵌套用例集");
});
});
afterEach(()=>{
cy.log("每个用例执行完成后,均会执行afterEach");
});
after(()=>{
cy.log("所有用例运行完成后执行after,仅执行一次");
});
});
最终的运行结果如下所示:
从上面示例中可以看出,标记为被排除的测试集后,则该测试集所有测试用例均不被执行
在实际项目中,only()和skip()通常是结合着使用,这时我们要先看标记describe的是skip()还是only(),其规则如下所示:
- 如果先标记describe的是skip(),则整个测试用例集都将被排除,不执行
- 如果先标记describe的是only(),若该用例集下,没有任何标记skip的用例,则所有用例都将运行
- 如果先标记describe的是only(),若该用例集下,如果有标记only的用例,则将仅运行标记了only的用例
- 如果describe和it均没有skip/only,则默认运行所有测试集或用例
4.4 动态执行测试用例
前面学习如何排除和执行指定测试集/测试用例,但在一些实际项目中,在执行用例的时候,会根据某一个条件来动态执行测试用例。为了解决这个问题,Cyrpess也提供相应的功能,通过更改相应的测试配置来支持。其语法格式如下所示:
describe(name, config, fn)
context(name, config, fn)
it(name, config, fn)
specify(name, config, fn)
部分配置参数为只读,不支持修改,可参考官网:https://docs.cypress.io/guides/references/configuration##Test-Configuration
- 1.测试用例集配置
示例代码如下所示:
///
describe('When not in Chrome',{browser:"!chrome"}, () => {
let baseUrl="https://example.cypress.io/todo";
it.only('Show warnings', () => {
cy.visit(baseUrl,{timeout:20000});
cy.get(".header input").should("have.class","new-todo");
});
it.only('测试用例-2', () => {
cy.visit(baseUrl,{timeout:20000});
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
});
在Chrome中运行时其结果如下所示:
下面的示例,将仅运行在指定的浏览器中,且指定的分辨率和环境变量将覆盖默认的值,如下所示:
///
describe('When in Chrome',
{
browser:"chrome",
viewportWidth: 800,
viewportHeight: 600,
env: {
flag: true,
url: "https://www.surpassme.com",
},
},
() => {
let baseUrl="https://example.cypress.io/todo";
it.only('Show warnings', () => {
cy.visit(baseUrl,{timeout:20000});
cy.get(".header input").should("have.class","new-todo");
});
it.only('测试用例-2', () => {
cy.visit(baseUrl,{timeout:20000});
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
});
运行时其结果如下所示:
- 2.单个测试用例配置
示例代码如下所示:
///
describe('When in Chrome', () => {
let baseUrl="https://example.cypress.io/todo";
it.only('Show warnings',
{
retries: {
runMode: 3,
openMode: 2
}
},
() => {
cy.visit(baseUrl,{timeout:20000});
cy.get(".header input").should("have.class","new-todo");
});
it.only('测试用例-2', () => {
cy.visit(baseUrl,{timeout:20000});
cy.get('.new-todo').type('todo A{enter}').type('todo B{enter}');
cy.get('.todo-list li').should('have.length', 4);
});
});
4.5 动态生成测试用例
在实际项目中,会遇到一种情况,就是多个用例的操作步骤、断言均一样,仅是输入和输出不一样。如果我还是一条数据写一个用例,则效率会非常低。此时,我们可以利用Cypress动态生成测试用例来高效完成。我们以登录为例,如下所示:
- 1.创建一个postdata.json格式的数据文件
[
{
"title": "测试数据-1",
"customerName": "Surpass",
"telephone": "18812345678",
"email":"[email protected]"
},
{
"title": "测试数据-2",
"customerName": "kevin",
"telephone": "17808231169",
"email":"[email protected]"
}
]
- 2.创建测试用例
///
import data from "./postdata.json"
describe("测试发送数据", () => {
let baseUrl="http://httpbin.org/forms/post";
beforeEach(()=>{
cy.visit(baseUrl)
});
data.forEach( item => {
it(item.title, () => {
cy.log(item.title,item.customerName,item.telephone)
cy.get(":nth-child(1) > label > input").type(item.customerName).should("contain.value",item.customerName);
cy.get("form > :nth-child(2) > label > input").type(item.telephone).should("contain.value",item.telephone);
cy.get("form > :nth-child(3) > label > input").type(item.email).should("contain.value",item.email);
});
})
})
运行时其结果如下所示:
如果大家对数据驱动比较了解的话,从上面的示例可以看到,是不是非常像?其最大价值为测试数据更改时,不影响测试代码。
4.6 断言风格
Cypress支持两种风格的断言 **BDD (expect/should) ** 和 TDD (assert) ,以下列举常见的断言:
- 1、针对长度(Length)的断言
cy.get('.todo-list li').should('have.length', 4);
- 2、针对类(Class)的断言
cy.get("form").find("input").should("not.have.class","surpass");
- 3、针对值(Value)的断言
cy.get("form > :nth-child(3) > label > input").should("contain.value","Surpass");
- 4、针对文本内容(Text)的断言
cy.get("button").should("contain.text","Submit order");
// 或
cy.get("button").should("not.contain","Surpass");
- 5、针对元素是否可见(Visible)的断言
cy.get("button").should("be.visible");
- 6、判断元素是否存在的断言
cy.get("button").should("not.exist");
- 7、判断元素状态(State)的断言
cy.get(":nth-child(4) > :nth-child(2) > label > input").click().should("be.checked");
- 8、针对CSS的断言
cy.get(".surpass").should("have.css","line-surpass");
4.7 运行测试用例
4.7.1 运行单个用例
运行单个测试用例,可以获得到更好的性能。在Test Runner界面, 点击单个JS文件 即可,其操作方法如下所示:
4.7.2 运行所有用例
运行所有测试用例,可以在Test Runner中单击运行 Run x integration specs
4.7.3 运行部分用例
可以通过 筛选 从而仅运行符合筛选条件的用例。
筛选条件中忽略大小写
4.8 测试运行状态
在Cypress中,测试用例的运行状态可以分为4种状态: 通过(Passed) 、 失败(Failed) 、 等待(Pending) 、 跳过(Skipped) 。
4.8.1 Passed
Passed表示测试用例的运行状态为通过,没有出现断言失败的情况。如下所示:
4.8.2 Failed
Failed表示测试用例的运行状态为失败,有出现断言失败的情况。如下所示:
4.8.3 Pending
当一个测试用例没有想好怎么写时,我们可以写一些占位性质的用例,虽然Cypress不会运行,但却会让后续的用例状态处于Pending状态,如下所示:
///
describe('测试Pending场景', () => {
it('未完成的测试用例')
it.skip('已经完成的用例', () => {
let url="http://httpbin.org/forms/post";
let customerName="Surpass",telephone="18812345678",email="[email protected]",textMsg="I Love Surpass",buttonText="Submit order";
cy.visit(url,{timeout:10000});
cy.get(":nth-child(1) > label > input").type(customerName).should("contain.value",customerName);
cy.get("form > :nth-child(2) > label > input").type(telephone).should("contain.value",telephone);
cy.get("form > :nth-child(3) > label > input").type(email).should("contain.value",email);
cy.get("textarea").type(textMsg).should("contain.value",textMsg);
cy.get("button").should("contain.text",buttonText)
});
xit("另一种测试用例",()=>{
expect(false).to.true;
});
});
运行时其结果如下所示:
以上三种情况的用例均会标识为Pending状态
如果用例编写人员故意使用以上三种方式中的其中一种方式来跳过执行用例,则Cypress将视为Pending Test
4.8.4 Skipped
当一个测试用例本来要执行,但却因为某些原因而未能运行,则Cypress会将该用例的运行状态视为Skipped。我们在一个测试集里面添加beforeEach(),示例代码如下所示:
///
describe('测试用例状态为Skipped场景', () => {
let baseUrl="https://example.cypress.io/todo";
beforeEach(()=>{
cy.visit(baseUrl);
});
it('hides footer initially', () => {
cy.get('.filters').should('be.exist');
});
it('adds 2 todos', () => {
cy.get('.new-todo').type('learn testing{enter}').type('be cool{enter}');
cy.get('.todo-list li').should('have.length', 4);
})
});
以上这种情况,所有测试用例能顺利执行并通过,如下所示:
我们修改以上代码,让beforeEach()访问的地址不存在,再来看看,如下所示:
///
describe('测试用例状态为Skipped场景', () => {
let baseUrl="https://example.cypress.io/todo";
beforeEach(()=>{
cy.visit(baseUrl+"/does-not-exist");
});
it('hides footer initially', () => {
cy.get('.filters').should('be.exist');
});
it('adds 2 todos', () => {
cy.get('.new-todo').type('learn testing{enter}').type('be cool{enter}');
cy.get('.todo-list li').should('have.length', 4);
})
});
运行结果如下所示:
在上述示例代码中,用例adds 2 todos本来想运行,但却因为beforeEach出现问题,而未能运行,最终被标识为Skipped。
4.9 Test Runner简介
我们先来看看Test Runner的截图如下所示:
Cypress自带的Test Runner功能非常强大,允许在测试运行期间查看测试命令的执行情况,并监控在命令执行时,被测试程序所处的状态。其组成部分如下所示:
- 1.测试状态目录
在上图 标识为1的区域 ,用于展示测试用例运行成功、失败的数量、运行时间、禁用自动滚动和重新运行按钮等
- 2.运行命令日志
在上图 标识为2的区域 ,用于记录每个执行的命令。鼠标单击,可以在应用程序预览(标识为5的区域)中查看其对应的元素及其执行详细信息。
- 3.元素定位辅助器
在上图 标识为3的区域 ,通过该功能可以快速定位元素。如下所示:
- 4.URL预览栏
在上图 标识为4的区域 ,通过URL预览栏,可以查看被测试程序运行进所对应的URL地址。
- 5.应用程序预览
在上图 标识为5的区域 ,通过应用程序预览窗口,可以展示应用程序在测试运行中的实时状态
- 6.视窗大小窗口
在上图 标识为6的区域 ,通过视窗大小可以了解当前应用程序运行时的视窗大小。如下所示: