iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 10
5

Nightwatch.js

Nightwatch 的 BDD Expect 是源自於 Chai 的 Expect API,並且只能用於網頁元素的比對。expectassert 更有彈性和口語化,缺點是不能串起來(chain)使用。

本系列文章皆使用這個專案,可以拉下來玩玩;有什麼問題都可以提出 issue


名詞解釋

欸,前言就提到了一堆陌生的專有名詞,坐在隔壁的露天廢物成員 hunterliu1003 表示生氣(翻桌?) (╯‵□′)╯︵┴─┴

「不是說好是手牽手一起學 Nightwatch 嗎?」 (☍﹏⁰)

那麼就來一個個好好解釋清楚吧。

什麼是 BDD?

BDD 是指行為驅動開發(Behavior-Driven Development)意即在開發前先撰寫測試程式,以確保程式碼品質符合驗收規格。除了實作前先寫測試外,還要寫一份「可以執行的規格」。白話文就是使用者想看到什麼、打開什麼、點到什麼,就這麼寫在測試程式裡面。

像是這樣...

  • 進入首頁即可看到一個紅色的按鈕(O)
  • 在試算頁面依序點擊按鈕「1」、「+」、「2」、「=」,輸入框裡面的文字為「3」(O)
  • 將兩個數字相加得到結果,期待函式 add(1, 2) 回傳得到 3(X,並非以使用者可執行的角度撰寫規格)

BDD 其實是一種 TDD,最大的差異在於

  • BDD:從使用者的角度去思考驗收規格
  • TDD:從測試結果去思考程式該如何實作

不論 BDD 或 TDD 都只是在談理念,它們並不是真正實作的方法喔。

總整理,來比較 TDD 與 BDD 的差異。

TDD BDD
全名 測試驅動開發Test-Driven Development 行為驅動開發Behavior Driven Development
定義 在開發前先撰寫測試程式,以確保程式碼品質與符合驗收規格。 TDD 的進化版。除了實作前先寫測試外,還要寫一份「可以執行的規格」。
特性 從測試去思考程式如何實作。強調小步前進、快速且持續回饋、擁抱變化、重視溝通、滿足需求。 從用戶的需求出發,強調系統行為。使用自然語言描述測試案例 ,以減少使用者和工程師的溝通成本。測試後的輸出結果可以直接做為文件閱讀。

參考認試軟體測試的世界 & TDD/BDD 入門

Chai

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 用於提高可讀性,但沒有任何測試上的功能,並且沒有順序關係,只是用來連接取到的元素和斷言。可以想像成說話上常用的連接詞,像是「然後」、「接著」、「的」,用了讓人覺得流暢,不用也仍能聽懂看懂。

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 出現,並檢視其中文字是否與「露天旗艦店」相同。
  • 結束這個 session,關閉瀏覽器。
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();
  }
};

完整範例程式碼在這裡

BDD Expect

.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

BDD BDD Expect before

.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

BDD BDD Expect after

.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 是啟用的
  • 結束 session,關閉瀏覽器
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

執行結果。

BDD Expect

以上看起來其實很雜亂 ◢▆▅▄▃崩╰(〒皿〒)╯潰▃▄▅▇◣

話說工程師都愛模組化,不要擔心亂糟糟,待之後使用 Page Objects 來改寫 (*´∀`)~♥

下一篇來看 BDD Assert


網誌版


上一篇
Nightwatch101 #9:指令 Part 3
下一篇
Nightwatch101 #11:BDD Assert
系列文
Nightwatch101:使用 Nightwatch 實現 End-to-End Testing30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

2
HunterLiu
iT邦新手 4 級 ‧ 2017-12-22 10:01:19

/images/emoticon/emoticon12.gif

Summer iT邦新手 3 級 ‧ 2017-12-22 10:02:23 檢舉

/images/emoticon/emoticon58.gif

我要留言

立即登入留言