你不知道的Cypress系列(3) -- 是时候重构自己的思维了!

2021-01-04 14:35:14 浏览数 (1)

在跟同学们的交流中,我也了解到, 原来除了国外优秀的公司(例如Adobe, 迪士尼,AutoDesk等等), 国内也有很多公司在尝试使用Cypress提升测试效率。

在Cypress中国群内、在公众号iTesting里,我每天都能看到大量关于Cypress的使用讨论和私下问询。这让我感到无比荣幸。(买了书的同学们,公众号回复你的微信号,拉你到Cypress中国群)。

除了日常推荐大家通过阅读我的书来解决日常Cypress使用问题外,我也一直在更新着我这边的Cypress知识图谱, 不夸张的说,目前我总结和实践下来知识点多达200多篇。

本着“雕琢自我,普惠他人”的原则,我决定在公众号iTesting上开设<你不知道的Cypress系列>专栏。此专栏目的是分享一些我自己趟过的坑,走过的弯路、以及在选型时抛弃了的实践。希望让大家在选用Cypress作为前端自动化测试框架方案时, 可以借鉴一下,避免再走我走过的弯路。

今天是<你不知道的Cypress系列>的第三篇 -- 是时候重构自己的思维了!

由于Selenium/WebDriver的“荼毒”, 当前在自动化过程中,很多不合理的操作,反而都变成了标准流程。

例如,要进行元素属性值比较,我们首先想到的就是先赋值,再比较。

再比如,自动化过程中,常常会陷入”条件测试(Conditional Testing)“的陷阱! 例如,我见过太多这样的case:”如果我点击了某button,如果弹出框没有出现,我执行A操作,如果出现,我执行B操作“。

由于这种“荼毒”,初次使用Cypress时, 大多数同学都会认为自己掉进坑里了!

下面我们来一个个分析:

(一)诡异的赋值

01

赋值不起作用

赋值操作是最常见的了,赋值最常用的场景是获取元素的某个属性供以后使用。

没接触过JavaScript的同学,在第一次写Cypress脚本时,一定会遇见如下问题:

代码语言:javascript复制
describe('欢迎关注iTesting', function () {

    it('你以为的赋值', function () {
        // 定义变量那么,用来获取button的text是什么
        let name
        cy.visit('/login')
        //关注公众号iTesting, 玩转Cypress
        cy.get('button[type="submit"]').then(obj => {
            name = obj.text()
            // 第一次打印
            cy.log('循环内有值', name)
        })
        // 第二次打印
        cy.log('循环外没有值', name)
    })
})

如上述代码所示,我定义了一个变量name,并尝试把submit button的text即“Submit” 赋值给name。 上面的代码看起来没有任何毛病,但是运行时,你会发现我第一次打印时有值, 但是二次打印时name的值是null。

02

赋值不起作用的原因

写惯了Python或Java的同学往往会卡在这里觉得莫名其妙。其实也就是同步执行和异步执行的差异了。

同步执行

可以简单理解为,当你执行一个操作,在这个操作没有结果之前,其后续的操作不会执行。

异步执行:

可以简单理解为,当你执行一个操作后,其后续的操作可以立即执行, 当这个操作有结果后,再通过状态,通知或者回调来通知这个操作的调用方。

这个解释相当之不准确,不过也足够我们继续下面的内容了。 你如果感兴趣, 可以搜索同步、异步、阻塞、非阻塞来了解更多进程通信和系统调用的知识。

正常情况下,Python代码,Java代码就是同步执行的,JavaScript代码就是异步执行的。

了解了这一点,你就明白了,当执行到第13行时,name的值还没有被返回,所以打印不出来。

03

99%的情况都无须赋值

使用Selenium/WebDriver比较熟悉的同学,初次转到Cypress后,很容易就自无劝退:”Cypress好难用, 我还是用回Selenium/WebDriver“吧。

拿对元素属性值进行断言为例,大家很容易就沿用Selenium/WebDriver时代的旧思维,认为,必须先拿出元素的属性值赋给一个变量,然后在用这个变量跟给定的期望结果对比。实际上,根本无需如此!

在Cypress中,99%的操作都无须赋值!

下面分别举例:

Selenium/WebDriver

代码语言:javascript复制
//获取元素的属性值,并比较
value = driver.find_element_by_id('kw').get_attribute('innerHTML')
assert value == "iTesting"

Cypress:

代码语言:javascript复制
//获取元素的属性值,并比较
cy.get('#kw').should('have.text', 'iTesting')

就这么简单!

(二)Cypress命令是如何运行的?

01

先来看一个大家常常会犯的错误:

假设我们定义了一个自定义方法login,最后返回登录后的凭证:

代码语言:javascript复制
// cypress/support/index.ts
Cypress.Commands.add('login', (username, password) => {
  //各种代码实现登录
  // 后返回登录凭证
  return auth
})

然后在测试用例里,经常看到这样的使用方式:

代码语言:javascript复制
const auth = cy.login("iTesting", "iTesting")

最后会发现auth的值是undefined,很多人百撕不得骑姐。

为什么?

这是因为Cypress命令在它们被调用时不会执行任何操作。它们会自我排队(“enqueue themselves”),最后在统一运行。

来看下官方的解释:

代码语言:javascript复制
it('iTesting邀你看Cypres如何运行', () => {
  cy.visit('/my/resource/path') // 啥也不干

  // 关注iTesting,玩转Cypress
  cy.get('.awesome-selector')   // 还是啥也不干
    .click()                    // 必须啥也不干

  cy.url()                      // 别想了,就是不干
    .should('include', '/hello') // 就在此刻,干!
})

// 好了,运行完了, 哦, 你真是一个快枪手。
// 事实上,所有的Cypress命令会被queue起来,直到所有命令被chain完毕。
// 然后Cypress开始按它们被queue的顺序开始运行。

这个就是Cypress的魔力。这就是为什么JavaScript是异步执行的,但是Cypress命令却能按照你的代码“顺序“执行的原因!

02

那么,知道了Cypress命令是如何运行的,再来看上面的登录例子,你就知道了,

代码语言:javascript复制
const auth = cy.login("iTesting", "iTesting")

cy.login没有被执行,当然auth里就没有值了。

那么,如何才能确保cy.login被执行呢?

为了让你能够访问到Cypress命令执行的结果,Cypress提供了

代码语言:javascript复制
.then()

.then是闭包的一个典型应用。(如果想深入理解,可以查下Promise)

所以,这下,我们知道要访问cy.login的返回值,我们应该这样用:

代码语言:javascript复制
cy.login("iTesting", "iTesting").then( auth => {
const secret = auth
//剩余用到auth的代码。
})

这下,你就能愉快的使用Cypress命令的返回值了,不过也带来一个问题,就是代码层次比较深。。。

(三)拒绝条件测试

01

前面我提到了条件测试(Conditional Testing),实际上,条件测试常见常景如下:

代码语言:javascript复制
1. 我想在元素存在或者不存在时,执行不同的操作。
2. 我的应用程序有A/B Testing,我需要测试到不同的分支。

为了实现这个功能,在Selenium/WebDriver编程中,我们大量使用if...else,我们以为我们Cover住这种情况了,结果我们就发现我们的测试会薛定谔成功:有时候执行能成功,有时候执行不成功, 在你不执行的时候你永远不知道到底执行能不能成功。

我们来看一个例子:

假设你的应用程序代码如下:

代码语言:javascript复制
// 你的应用程序代码

// 定义一个随机时间
const random = Math.random() * 100

// 创建一个 <button> 元素
const btn = document.createElement('button')

//关注iTesting,玩转Cypress
// attach 这个元素到body上
document.body.appendChild(btn)

setTimeout(() => {
  // 在random的时间内,给btn一个类属性。
  btn.setAttribute('class', 'active')
}, random)

你的测试代码就会如下:

代码语言:javascript复制
it('薛定谔的测试', () => {
 // 多执行几遍,你发现,有时候btn的属性是active,有时候不是active。
  cy.get('button').then(($btn) => {
    if ($btn.hasClass('active')) {
      //  active时的代码
    } else {
      // 非active的代码
    }
  })
})

这也是Selenium/WebDriver被诟病的原因之一,不稳定!

02

为了避免这个情况,Cypress告诉你, 不要去做条件测试(Conditional Testing)!

Cypress说,既然你在测试,那么你就应该知道你的每一步下去,其结果是什么。如果你不能确定你的操作下去结果是什么,那么你就不是在测试!(没毛病吧)

相应的,你就要调整你的测试策略,尽量避免让自己的代码处于条件测试(Conditional Testing)下, 具体来说就一句话:

代码语言:javascript复制
事先做一些操作,确保你的某个操作一定只有一个结果!

如何做到呢?别忘记,Cypress是运行在浏览器之内的,是跟你的应用程序运行在同一个生命周期的,你对你的应用程序有完全的控制权!

听起来很好,不过很可惜。这句话主要是对开发说的,对我们QA来说,用处不大(因为我们QA还是不知道改哪行代码啊!)。

不过,这里还是有一些原则, 比如:

代码语言:javascript复制
1. A/B Testing, 可以根据AB的策略,构造出一定会走A逻辑的测试数据。
2. 判断元素在不在,一定可以根据业务知道你的什么操作,它一定会在。

总结

当你初次使用Cypress时,特别是当你是从Selenium/WebDrvier转到Cypress来时,你一定会感觉到不习惯。这是必然的。

当你遇见问题时,不妨尝试转换下思维,把老的思维模式抛弃掉,转入到Cypress的思维中来,毕竟,我们做测试是为了:

测试你的代码,而不是你的耐心!

0 人点赞