Nightwatch 的 BDD Expect 是源自於 Chai 的 Expect API,並且只能用於網頁元素的比對。expect
比 assert
更有彈性和口語化,缺點是不能串起來(chain)使用。
本系列文章皆使用這個專案,可以拉下來玩玩;有什麼問題都可以提出 issue。
欸,前言就提到了一堆陌生的專有名詞,坐在隔壁的露天廢物成員 hunterliu1003 表示生氣(翻桌?) (╯‵□′)╯︵┴─┴
「不是說好是手牽手一起學 Nightwatch 嗎?」 (☍﹏⁰)
那麼就來一個個好好解釋清楚吧。
BDD 是指行為驅動開發(Behavior-Driven Development)意即在開發前先撰寫測試程式,以確保程式碼品質符合驗收規格。除了實作前先寫測試外,還要寫一份「可以執行的規格」。白話文就是使用者想看到什麼、打開什麼、點到什麼,就這麼寫在測試程式裡面。
像是這樣...
BDD 其實是一種 TDD,最大的差異在於
不論 BDD 或 TDD 都只是在談理念,它們並不是真正實作的方法喔。
總整理,來比較 TDD 與 BDD 的差異。
TDD | BDD | |
---|---|---|
全名 | 測試驅動開發Test-Driven Development | 行為驅動開發Behavior Driven Development |
定義 | 在開發前先撰寫測試程式,以確保程式碼品質與符合驗收規格。 | TDD 的進化版。除了實作前先寫測試外,還要寫一份「可以執行的規格」。 |
特性 | 從測試去思考程式如何實作。強調小步前進、快速且持續回饋、擁抱變化、重視溝通、滿足需求。 | 從用戶的需求出發,強調系統行為。使用自然語言描述測試案例 ,以減少使用者和工程師的溝通成本。測試後的輸出結果可以直接做為文件閱讀。 |
Chai 提供測試用的斷言庫(Assertion Library)。斷言庫是一種判斷工具,驗證執行結果是否符合預期,若實際結果和預測不同,就是測到 bug 了。
例如
預期 2 等於 2。
expect(2).to.equal(2);
預期「foo」等於「bar」,若不相等就報錯「foo is not bar」。
assert('foo' === 'bar', 'foo is not bar');
有興趣可以參考這裡,內文有比較詳細的說明。
Chainable Getters 用於提高可讀性,但沒有任何測試上的功能,並且沒有順序關係,只是用來連接取到的元素和斷言。可以想像成說話上常用的連接詞,像是「然後」、「接著」、「的」,用了讓人覺得流暢,不用也仍能聽懂看懂。
Chainable Getters 有 to、be、been、is、that、which、and、has、have、with、at、does、of。
如下所示,檢視這個 DOM element 的 inner text 是否為「露天旗艦店」。
browser.expect.element('.rt-flagship .rt-ad-heading').text.to.equal('露天旗艦店');
將「to」改為「be」也是可以的。
browser.expect.element('.rt-flagship .rt-ad-heading').text.be.equal('露天旗艦店');
完整範例程式碼在這裡。
備註:開頭提到 expect
不能「串起來」(chain),這是指 不能 這樣接續使用
browser
.expect.element('.text-1').text.to.equal('title 1')
.expect.element('.text-2').text.be.equal('title 2')
和 Chainable Getters 沒有任何的關係喔!不要錯亂了。
以下就來進入正題,來看看 Nightwatch 所提供的 BDD Expect 斷言指令。
.equal(value)
等於 / .contain(value )
包含 / .match(regex)
符合條件對指定的 DOM element 執行斷言,作為比較的目標值可為 HTML 屬性值、元素內的文字或 css 屬性值等。
例如,檢視 #text
的 inner text 是否為 「Hello World!」。
browser.expect.element('#text').text.to.equal('Hello World!');
檢視 #text
的 inner text 是否包含「Hello World!」。
browser.expect.element('#text').text.to.contain('Hello World!');
檢視 #text
的 inner text 是否以「H」開頭。
browser.expect.element('#text').text.to.match(/^H/);
<body>
出現。.rt-flagship .rt-ad-heading
DOM element 出現,並檢視其中文字是否與「露天旗艦店」相同。module.exports = {
'Assert Ruten MainCategory Title': browser => {
browser.url('http://class.ruten.com.tw/category/main?0008');
browser.waitForElementVisible('body');
browser.expect.element('.rt-flagship .rt-ad-heading').text.to.equal('露天旗艦店');
browser.end();
}
};
完整範例程式碼在這裡。
.not
否定,可表示為不等於、不包含。
#text
這個 DOM element 的 inner text 是否不為「Hello World!」。
browser.expect.element('#text').text.to.not.equal('Hello World!');
#text
這個 DOM element 的 inner text 是否不包含「Hello World!」。
browser.expect.element('#text').text.to.not.contain('Hello World!');
#text
這個 DOM element 的樣式中,display
的值是否不為「block」。
browser.expect.element('#text').to.have.css('display').which.does.not.equal('block');
.before(ms)
/ .after(ms)
在指定時間前或後重新執行斷言,其後可串接其他判斷,增加重試的機會。
.before(ms)
檢視 .rt-flagship .rt-ad-heading
這個 DOM element 的 inner text 是否為「露天旗艦店」、是否包含「露天」,並在 0.5 秒後重新檢視一次。
範例程式碼。
module.exports = {
'Assert Ruten MainCategory Title Again (before)': browser => {
browser.url('http://class.ruten.com.tw/category/main?0008');
browser.waitForElementVisible('body');
browser.expect.element('.rt-flagship .rt-ad-heading').text.to.equal('露天旗艦店').text.to.contain('露天').before(500);
browser.end();
}
}
執行以下範例。
nightwatch test/e2e/class/testMainCategory.js
.after(ms)
檢視 .rt-flagship .rt-ad-heading
這個 DOM element 的 inner text 是否為「露天旗艦店」、是否包含「露天」,並在 1 秒後重新檢視一次。
範例程式碼。
module.exports = {
'Assert Ruten MainCategory Title Again': browser => {
browser.url('http://class.ruten.com.tw/category/main?0008');
browser.waitForElementVisible('body');
browser.expect.element('.rt-flagship .rt-ad-heading').text.to.equal('露天旗艦店').after(1000).text.to.contain('露天');
browser.end();
}
}
執行範例。
nightwatch test/e2e/class/testMainCategory.js
.a(type)
/ .an(type)
檢視 DOM element 的 tag name / type 是否為預期的值。例如:#text
是否為 <div>
。
預期 #text
是 <div>
。
browser.expect.element('#text').to.be.a('div');
預期 #text
是 <input>
,若不是則報錯「Testing if #text is an input」。
browser.expect.element('#text').to.be.an('input', 'Testing if #text is an input');
.attribute(name)
檢視 DOM element 的是否存在特定的 HTML attribute,若不存在則可顯示客製化錯誤訊息。
body
是否含有 attribute data-attr
。
browser.expect.element('body').to.have.attribute('data-attr');
body
是否「不」含有 attribute data-attr
。
browser.expect.element('body').to.not.have.attribute('data-attr');
body
是否「不」含有 attribute data-attr
,若存在就顯示客製化錯誤訊息「Testing if body does not have data-attr」。
browser.expect.element('body').to.not.have.attribute('data-attr', 'Testing if body does not have data-attr');
body
是否「不」含有 attribute data-attr
,並在 0.1 秒後重新檢查。
browser.expect.element('body').to.have.attribute('data-attr').before(100);
body
是否含有 attribute data-attr
,並且這個 attribute 的名稱為「some attribute」。
browser.expect.element('body').to.have.attribute('data-attr').equals('some attribute');
body
是否含有 attribute data-attr
,並且這個 attribute 的名稱不為「other attribute」。
browser.expect.element('body').to.have.attribute('data-attr').not.equals('other attribute');
body
是否含有 attribute data-attr
,並且這個 attribute 的名稱包含字串「something」。
browser.expect.element('body').to.have.attribute('data-attr').which.contains('something');
body
是否含有 attribute data-attr
,並且這個 attribute 的名稱以字串「something else」為開頭。
browser.expect.element('body').to.have.attribute('data-attr').which.matches(/^something\ else/);
這是錯的,因為只能檢查屬性是否存在,而不是檢查其值。
browser.expect.element('.good-shops .rt-ad-control-link:nth-child(2)').attribute('href').contain('point'); // 錯誤的寫法
.css(property)
檢視 DOM element 是否有指定的 css 屬性,若不存在可顯示客製化錯誤訊息。
browser.expect.element('#text').to.have.css('display'); // 是否有 display 屬性
browser.expect.element('#text').to.have.css('display', 'Testing for display'); // 是否有 display 屬性,若無則報錯
browser.expect.element('#text').to.not.have.css('display'); // 是否沒有 display 屬性
browser.expect.element('#text').to.have.css('display').which.equals('block'); // 是否有 display 屬性,並且其值為 block
browser.expect.element('#text').to.have.css('display').which.contains('some value'); // 是否有 display 屬性,並且其值包含 some value
browser.expect.element('#text').to.have.css('display').which.matches(/some\ value/); // 是否有 display 屬性,並且其值包含 some value
.enabled
檢視 DOM element 目前是否 enabled。
browser.expect.element('#input').to.be.enabled;
browser.expect.element('#input').to.not.be.enabled;
.present
檢視 DOM element 是否存在,但不一定是可見的。若要檢視是否可見,要使用 .visible
。
browser.expect.element('#text').to.be.present;
browser.expect.element('#text').to.not.be.present;
.selected
確認 <input>
element 的 radio 、checkbox 或 option element 被選取。
browser.expect.element('#option').to.be.selected;
browser.expect.element('#option').to.not.be.selected;
.text
取得 DOM element 的 inner text,後續可串連其他斷言的動作,例如:.equal(value) / .contain(value ) / .match(regex)
。
.value
取得 DOM element 的值,後續可串連其他斷言的動作,例如:,例如:.equal(value) / .contain(value ) / .match(regex)
。
.visible
檢視 DOM element 是否可見,可見就必定存在。若只是要檢視是否存在,使用 .present
即可。
檢視露天拍賣的頂層分類頁。
動作列舉如下
<body>
可見.rt-ad-text-only .rt-ad-item
這個 DOM Element 的 CSS 屬性 display 的值是 none.promo-bar
這個 DOM Element 存在.rt-subcategory-list .rt-subcategory-item
這個 DOM Element 存在#ad-flash
這個 DOM Element 可見#ad-flash .rt-ad-item
這個 DOM Element 存在.rt-flagship
這個 DOM Element 可見.rt-flagship .rt-ad-item
這個 DOM Element 存在#ad-promote .promoted-item
這個 DOM Element 存在#ad-special .special-item
這個 DOM Element 存在.hot-sale-item
這個 DOM Element 存在#ad-gallery .hot-sale-gallery-item
這個 DOM Element 存在.good-shop
這個 DOM Element 可見.good-shops .rt-ad-control-link
這個 DOM Element含有屬性 href.good-shops .rt-ad-item
這個 DOM Element 可見#ad-featured-list .rt-ad-item
這個 DOM Element 可見.top-sell
這個 DOM Element 存在.top-sell .rt-ad-item
這個 DOM Element 存在.shopping-mall
這個 DOM Element 存在.shopping-mall .rt-ad-item
這個 DOM Element 存在.shopping-mall .rt-ad-control-link
這個 DOM Element含有屬性 href.rt-ad-search-keyword
這個 DOM Element 存在.rt-ad-search-keyword .rt-ad-item
這個 DOM Element 存在#search_input
這個 DOM Element 是一個 input,若否則顯示客製化報錯 '#search_input should be an input'#search_input
這個 DOM Element 的值是空字串#search_input
這個 DOM Element 是啟用的.rt-site-search-submit
這個 DOM Element 是啟用的module.exports = {
'Assert Advertisements Status': browser => {
browser.url('http://class.ruten.com.tw/category/main?0008');
browser.waitForElementVisible('body');
browser.expect.element('.rt-ad-text-only .rt-ad-item').css('display').to.equals('none');
browser.expect.element('.promo-bar').to.be.present;
browser.expect.element('.rt-subcategory-list .rt-subcategory-item').to.be.present;
browser.expect.element('#ad-flash').to.be.visible;
browser.expect.element('#ad-flash .rt-ad-item').to.be.present;
browser.expect.element('.rt-flagship').to.be.visible;
browser.expect.element('.rt-flagship .rt-ad-item').to.be.present;
browser.expect.element('#ad-promote .promoted-item').to.present;
browser.expect.element('#ad-special .special-item').to.be.present;
browser.expect.element('.hot-sale-item').to.be.present;
browser.expect.element('#ad-gallery .hot-sale-gallery-item').to.be.present;
browser.expect.element('.good-shops').to.be.visible;
browser.expect.element('.good-shops .rt-ad-control-link').attribute('href');
browser.expect.element('.good-shops .rt-ad-item').to.be.visible;
browser.expect.element('#ad-featured-list .rt-ad-item').to.be.visible;
browser.expect.element('.top-sell').to.be.present;
browser.expect.element('.top-sell .rt-ad-item').to.be.present;
browser.expect.element('.shopping-mall').to.be.present;
browser.expect.element('.shopping-mall .rt-ad-item').to.be.present;
browser.expect.element('.shopping-mall .rt-ad-control-link').attribute('href');
browser.expect.element('.rt-ad-search-keyword').to.be.present;
browser.expect.element('.rt-ad-search-keyword .rt-ad-item').to.be.present;
browser.expect.element('#search_input').to.be.an('input', '#search_input should be an input');
browser.expect.element('#search_input').to.have.value.that.equals('');
browser.expect.element('#search_input').to.be.enabled;
browser.expect.element('.rt-site-search-submit').to.be.enabled;
browser.end();
}
}
這裡由 CSS Selector 取得的 DOM element 並不是一個集合,而是第一個符合選擇規格的元素。
執行以下範例。
nightwatch test/e2e/class/testMainCategoryExpect.js
執行結果。
以上看起來其實很雜亂 ◢▆▅▄▃崩╰(〒皿〒)╯潰▃▄▅▇◣
話說工程師都愛模組化,不要擔心亂糟糟,待之後使用 Page Objects 來改寫 (*´∀`)~♥
下一篇來看 BDD Assert。
網誌版。