共计 10709 个字符,预计需要花费 27 分钟才能阅读完成。
https://docs.cypress.io/guide…
describe('Post Resource', () => {it('Creating a New Post', () => {cy.visit('/posts/new') // 1.
cy.get('input.post-title') // 2.
.type('My First Post') // 3.
cy.get('input.post-body') // 4.
.type('Hello, world!') // 5.
cy.contains('Submit') // 6.
.click() // 7.
cy.url() // 8.
.should('include', '/posts/my-first-post')
cy.get('h1') // 9.
.should('contain', 'My First Post')
})
})
上述 cypress 代码,很像自然语言。
cypress 的 语法,cy.get(‘.my-selector’),很像 jQuery: cy.get(‘.my-selector’)
事实上,cypress 自身就 bundle 了 jQuery:
反对相似 jQuery 的链式调用:
cy.get('#main-content').find('.article').children('img[src^="/static"]').first()
只是有一点须要特地留神:
ct.get 并不会像 jQuery 那样,采纳同步的形式返回待读取的元素。Cypress 的元素拜访,采取 异步形式
实现。
因为 jQuery 的同步拜访机制,咱们在调用元素查问 API 之后,须要手动查问其后果是否为空:
// $() returns immediately with an empty collection.
const $myElement = $('.element').first()
// Leads to ugly conditional checks
// and worse - flaky tests!
if ($myElement.length) {doSomething($myElement)
}
而 Cypress 的异步操作,导致待读取的元素真正可用时,其后果才会被作为参数,传入回调函数:
cy
// cy.get() looks for '#element', repeating the query until...
.get('#element')
// ...it finds the element!
// You can now work with it by using .then
.then(($myElement) => {doSomething($myElement)
})
In Cypress, when you want to interact with a DOM element directly, call .then() with a callback function that receives the element as its first argument.
也就是说,Cypress 外部帮咱们封装了 retry 和 timeout 重试机制。
When you want to skip the retry-and-timeout functionality entirely and perform traditional synchronous work, use Cypress.$.
如果想回归到 jQuery 那种同步读取元素的格调,应用 Cypress.$ 即可。
// Find an element in the document containing the text 'New Post'
cy.contains('New Post')
// Find an element within '.main' containing the text 'New Post'
cy.get('.main').contains('New Post')
Cypress commands do not return their subjects, they yield them. Remember: Cypress commands are asynchronous and get queued for execution at a later time. During execution, subjects are yielded from one command to the next, and a lot of helpful Cypress code runs between each command to ensure everything is in order.
Cypress 命令并不会间接返回其工作的指标,而是 yield 这些指标。Cypress 命令以异步的形式执行,命令被插入到队列里,并不会立刻执行,而是期待调度。当命令真正执行时,指标对象经由前一个命令生成,而后传入下一个命令里。命令与命令之间,执行了很多有用的 Cypress 代码,以确保命令执行程序和其在 Cypress 测试代码里调用的程序统一。
To work around the need to reference elements, Cypress has a feature known as aliasing. Aliasing helps you to store and save element references for future use.
Cypress 提供了一种叫做 aliasing 的机制,能将元素援用保留下来,以备未来之用。
看一个例子:
cy.get('.my-selector')
.as('myElement') // sets the alias,应用 as 命令将 get 返回的元素存储到自定义变量 myElement 中。.click()
/* many more actions */
cy.get('@myElement') // re-queries the DOM as before (only if necessary),通过 @ 援用自定义变量
.click()
应用 then 来对前一个命令 yield 的指标进行操作
cy
// Find the el with id 'some-link'
.get('#some-link')
.then(($myElement) => {
// ...massage the subject with some arbitrary code
// grab its href property
const href = $myElement.prop('href')
// strip out the 'hash' character and everything after it
return href.replace(/(#.*)/, '')
})
.then((href) => {
// href is now the new subject
// which we can work with now
})
Cypress 的异步执行个性
It is very important to understand that Cypress commands don’t do anything at the moment they are invoked, but rather enqueue themselves to be run later. This is what we mean when we say Cypress commands are asynchronous.
it('changes the URL when"awesome"is clicked', () => {cy.visit('/my/resource/path') // Nothing happens yet
cy.get('.awesome-selector') // Still nothing happening
.click() // Nope, nothing
cy.url() // Nothing to see, yet
.should('include', '/my/resource/path#awesomeness') // Nada.
})
// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!
Cypress doesn’t kick off the browser automation magic until the test function exits.
这是 Cypress 不同于其余前端自动测试框架的特别之处:直到测试函数退出,Cypress 才会触发浏览器的主动执行逻辑。
it('does not work as we expect', () => {cy.visit('/my/resource/path') // Nothing happens yet
cy.get('.awesome-selector') // Still nothing happening
.click() // Nope, nothing
// Cypress.$ is synchronous, so evaluates immediately
// there is no element to find yet because
// the cy.visit() was only queued to visit
// and did not actually visit the application
let el = Cypress.$('.new-el') // evaluates immediately as []
if (el.length) {
// evaluates immediately as 0
cy.get('.another-selector')
} else {
// this will always run
// because the 'el.length' is 0
// when the code executes
cy.get('.optional-selector')
}
})
// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!
正确的做法,把 html 元素 evaluation 的代码放在 then 的 callback 里:
Each Cypress command (and chain of commands) returns immediately
每个 Cypress 命令 (蕴含命令链) 调用后立刻返回,不会阻塞住以达到同步运行的成果。
Having only been appended to a queue of commands to be executed at a later time.
这些 command 只是被增加到一个命令队列里,期待 Cypress 框架稍后对立调度执行。
You purposefully cannot do anything useful with the return value from a command. Commands are enqueued and managed entirely behind the scenes.
对于 Cypress 间接返回的命令的执行后果,咱们无奈对其履行任何无效的操作,因为代码里命令的调用,实际上只是退出到待执行队列里。至于何时执行,由 Cypress 对立调度,对 Cypress 测试开发人员来说是黑盒子。
We’ve designed our API this way because the DOM is a highly mutable object that constantly goes stale. For Cypress to prevent flake, and know when to proceed, we manage commands in a highly controlled deterministic way.
Cypress API 如此设计的起因是,DOM 是一种易变对象,随着用户操作或者交互,状态常常会 go stale. 为了避免出现 flake 情景,Cypress 遵循了上文形容的思路,以一种高度可控,确定性的形式来治理命令执行。
上面一个例子:网页显示随机数,当随机数跳到数字 7 时,让测试停下来。如果随机数不是数字 7,从新加载页面,持续测试。
下列是谬误的 Cypress 代码,会导致浏览器解体:
let found7 = false
while (!found7) {
// this schedules an infinite number
// of "cy.get..." commands, eventually crashing
// before any of them have a chance to run
// and set found7 to true
cy.get('#result')
.should('not.be.empty')
.invoke('text')
.then(parseInt)
.then((number) => {if (number === 7) {
found7 = true
cy.log('lucky **7**')
} else {cy.reload()
}
})
}
起因就是:在 while 循环里迅速将巨量的 get command 插入到工作队列 (精确的说是 test chain) 里,而基本没有机会失去执行。
The above test keeps adding more cy.get(‘#result’) commands to the test chain without executing any!
下面的代码,起到的成果就是,在 while 循环里,一直地将 cy.get 命令,退出到 test chain 里,然而任何一个命令,都不会有失去执行的机会!
The chain of commands keeps growing, but never executes – since the test function never finishes running.
命令队列里的元素个数持续增长,然而永远得不到执行的机会,因为 Cypress 代码自身始终在 while 循环里,没有执行结束。
The while loop never allows Cypress to start executing even the very first cy.get(…) command.
即便是工作队列里第一个 cy.get 语句,因为 while 循环,也得不到执行的机会。
正确的写法:
- 利用递归
- 在 callback 里书写找到 7 之后 return 的逻辑。
const checkAndReload = () => {
// get the element's text, convert into a number
cy.get('#result')
.should('not.be.empty')
.invoke('text')
.then(parseInt)
.then((number) => {
// if the expected number is found
// stop adding any more commands
if (number === 7) {cy.log('lucky **7**')
return
}
// otherwise insert more Cypress commands
// by calling the function after reload
cy.wait(500, { log: false})
cy.reload()
checkAndReload()})
}
cy.visit('public/index.html')
checkAndReload()
command 执行过程中背地产生的事件
下列这段代码,蕴含了 5 局部逻辑:
it('changes the URL when"awesome"is clicked', () => {cy.visit('/my/resource/path') // 1.
cy.get('.awesome-selector') // 2.
.click() // 3.
cy.url() // 4.
.should('include', '/my/resource/path#awesomeness') // 5.
})
5 个 步骤的例子:
- Visit a URL.
- Find an element by its selector.
- Perform a click action on that element.
- Grab the URL.
- Assert the URL to include a specific string.
上述 5 步骤 是 串行执行的,而不是并发执行。每个步骤背地,Cypress 框架都轻轻执行了一些“魔法”:
- Visit a URL
魔法:Cypress wait for the page load event to fire after all external resources have loaded
该命令执行时,Cypress 期待页面所有内部资源加载,而后页面抛出 page load 事件。
- Find an element by its selector
魔法:如果 find 命令没找到 DOM element,就执行重试机制,直到找到地位。 - Perform a click action on that element
魔法:after we wait for the element to reach an actionable state
在 点击元素之前,先期待其成为能够点击状态。
每个 cy 命令都有特定的超时工夫,记录在文档里:
https://docs.cypress.io/guide…
Commands are promise
This is the big secret of Cypress: we’ve taken our favorite pattern for composing JavaScript code, Promises, and built them right into the fabric of Cypress. Above, when we say we’re enqueuing actions to be taken later, we could restate that as “adding Promises to a chain of Promises”.
Cypress 在 promise 编程模式的根底上,削减了 retry 机制。
下列这段代码:
it('changes the URL when"awesome"is clicked', () => {cy.visit('/my/resource/path')
cy.get('.awesome-selector').click()
cy.url().should('include', '/my/resource/path#awesomeness')
})
翻译成 promise 格调的 JavaScript 代码为:
it('changes the URL when"awesome"is clicked', () => {
// THIS IS NOT VALID CODE.
// THIS IS JUST FOR DEMONSTRATION.
return cy
.visit('/my/resource/path')
.then(() => {return cy.get('.awesome-selector')
})
.then(($element) => {
// not analogous
return cy.click($element)
})
.then(() => {return cy.url()
})
.then((url) => {expect(url).to.eq('/my/resource/path#awesomeness')
})
})
Without retry-ability, assertions would randomly fail. This would lead to flaky, inconsistent results. This is also why we cannot use new JS features like async / await.
短少重试机制,结果就是造成 flaky 和不统一的测试后果,这就是 Cypress 没有抉择 async / await 的起因。
You can think of Cypress as “queueing” every command. Eventually they’ll get run and in the exact order they were used, 100% of the time.
Cypress 的命令执行程序和其被插入 test chain 队列的程序完全一致。
How do I create conditional control flow, using if/else? So that if an element does (or doesn’t) exist, I choose what to do?
有的开发人员可能会产生疑难,如何编写条件式控制流,比方在 IF / ELSE 分支里,执行不同的测试逻辑?
The problem with this question is that this type of conditional control flow ends up being non-deterministic. This means it’s impossible for a script (or robot), to follow it 100% consistently.
事实上,这种条件式的管制逻辑,会使测试散失去确定性(non-deterministic). 这意味着测试脚本挥着机器人,无奈 100% 严格依照测试程序去执行。
下列这行代码:
cy.get('button').click().should('have.class', 'active')
翻译成自然语言就是:
After clicking on this <button>, I expect its class to eventually be active.
留神其中的 ==eventually==.
This above test will pass even if the .active class is applied to the button asynchronously – or after a indeterminate period of time.
Cypress 会一直重试上述的 assertion,直至 .active class 被增加到 button 上,不论是通过异步增加,还是在一段未知长度的时间段后。
What makes Cypress unique from other testing tools is that commands automatically retry their assertions. In fact, they will look “downstream” at what you’re expressing and modify their behavior to make your assertions pass.
You should think of assertions as guards.
Use your guards to describe what your application should look like, and Cypress will automatically block, wait, and retry until it reaches that state.
Cypress 命令默认的 assertion 机制
With Cypress, you don’t have to assert to have a useful test. Even without assertions, a few lines of Cypress can ensure thousands of lines of code are working properly across the client and server!
This is because many commands have a built in Default Assertion which offer you a high level of guarantee.
很多 cy 命令都有默认的 assertion 机制。
- cy.visit() expects the page to send text/html content with a 200 status code. 确保 页面收回 text/html 内容后,收到 200 的状态码。
- cy.request() expects the remote server to exist and provide a response.
确保远端零碎存在,并且提供响应。 - cy.contains() expects the element with content to eventually exist in the DOM.
确保制订的 content 最终在 DOM 中存在。 - cy.get() expects the element to eventually exist in the DOM.
确保申请的 element 最终在 DOM 中存在。
- .find() also expects the element to eventually exist in the DOM. – 同 cy.get
- .type() expects the element to eventually be in a typeable state.
确保元素处于可输出状态。 - .click() expects the element to eventually be in an actionable state.
确保元素处于可点击状态。 - .its() expects to eventually find a property on the current subject.
确保以后对象上可能找到对应的 property
All DOM based commands automatically wait for their elements to exist in the DOM.
所有基于 DOM 的命令,都会主动阻塞,直至其元素存在于 DOM 树为止。
cy
// there is a default assertion that this
// button must exist in the DOM before proceeding
.get('button')
// before issuing the click, this button must be "actionable"
// it cannot be disabled, covered, or hidden from view.
.click()
在执行 click 命令之前,button 必须成为可点击状态,否则 click 命令不会失去执行。可点击状态(actionable),意思是 button 不能是 disabled,covered,或者 hidden 状态。
Cypress 命令自带的超时设置
cy.get('.mobile-nav').should('be.visible').and('contain', 'Home')
- Queries for the element .mobile-nav, 而后进展 4 秒,直至元素呈现在 DOM 里。
- 再进展 4 秒,期待元素呈现在页面上。
- 再期待 4 秒,期待元素蕴含 home 的 text 属性。
一段测试程序里的所有 Cypress 命令,共享同一个超时值。
更多 Jerry 的原创文章,尽在:” 汪子熙 ”: