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的原創文章,盡在:"汪子熙":