Nightwatch101
手牽手一起來學 Nightwatch!
Agenda
● Nightwatch 與 Selenium Webdriver
● 環境建置
● 設定檔
● 定位網頁元素:CSS Selector 與 Xpath
● Nightwatch Commands
● 斷言:BDD Expect、Assert、Verify
Agenda (cont.)
● Test Hooks
● Nightwatch Test Runner:分組、標籤、禁跑特定測試
● Page Objects
● 客製化指令與斷言
● 客製化測試報告
● 總結
露天拍賣。前端工程師
cythilya@gmail.com
https://cythilya.github.io
@cythilya
Summer
網站又壞了?
結帳折扣金
額有誤?
上架頁
不能改規格
首頁廣告
無法顯示
搜尋結果
出不來 XD
人工測試太辛苦了,
用 Nightwatch 自動處理吧
Nightwatch 簡介
● Nightwatch 與 Selenium Webdriver
● 環境建置
● 設定檔
Nightwatch
Nightwatch 是什麼?
● 網頁專用的自動化測試框架
End-to-End Testing 是什麼?
● 模擬使用者對瀏覽器進行操作,例如:瀏覽網址、輸入文字、點擊按鈕
● 可做 UI 測試、整合測試
Nightwatch 與 Selenium Webdriver
環境建置
● 安裝 Java Development Kit(JDK),版本 7+
● 安裝 Nightwatch
● 下載專案 https://goo.gl/mFHJ2c
● 使用 Test Runner 進行測試
○ nightwatch ./test/e2e/testDemo.js
Nightwatch Test Runner 設定檔
Nightwatch 提供了 Command-line Test Runner,用來跑各種類型的測試
例如:指定測試環境、依群組或標籤或個別檔案
● 設定檔在專案根目錄下
● 預設檔名是 nightwatch.json 或 nightwatch.conf.js(優先)
● 設定檔分為三個部分:基本設定(Page Objects、客製化指令和斷言的位置)、
Selenium Server 相關、測試環境相關
● External Globals:放置複雜的運算或 Plugin
範例
Nightwatch 指令與斷言
● 定位網頁元素:CSS Selector 與 Xpath
● 常用指令
● 斷言:BDD Expect、Assert、Verify
定位網頁元素
定位網頁元素有兩種方法
● CSS Selector
● Xpath
使用 CSS Selector 定位網頁元素
.waitForElementVisible('body', 1000) //等待 1 秒,確認 <body> 是否出現
.isVisible('.header') //確認 ".header" 是否可見
.getText('.link') //取得 ".link" 內的文字
.setValue('.input', 'Pusheen') //".input" 欄位輸入 "Pusheen"
使用 Xpath 定位網頁元素
● 設定檔 use_xpath: true 可設定使用 Xpath 為預設選取策略
● 切換使用 CSS Selector 或 Xpath
.useXpath() //使用 Xpath 來抓取網頁元素
.useCss() //使用 CSS Selector 來抓取網頁元素
//在帳號欄位輸入字串 nightwatch101
.setValue('//input[@id="userid"]', 'nightwatch101')
//在密碼欄位輸入字串 nightwatch101
.setValue('//input[@id="userpass"]', 'nightwatch101')
Nightwatch Commands
url, waitForElementPresent, setValue, click, pause, end
'Ruten Desktop Login': browser => {
browser
.url('https://member.ruten.com.tw/user/login.htm') // 打開指定網址
.waitForElementPresent('body') // 存在? 確認 DOM Element 載入完成
.setValue('#userid', 'nightwatch101') // 對 input text 鍵入字串
.setValue('#userpass', '*************')
.click('#btnLogin') // 點擊送出按鈕
.pause(1000) // 暫停測試程式,可指定暫停時間( ms)
.end(); // 結束 session,關閉瀏覽器
}
nightwatch test/e2e/member/testDesktopLogin.js
Nightwatch Commands (cont.)
waitForElementVisible, getTitle, isVisible
'Demo Ruten SubCategory Page': browser => {
browser
.url('http://class.ruten.com.tw/category/sub00.php?c=00080001')
.waitForElementVisible('body') // <body> 可見?
.getTitle(function(title) { // 取得網頁標題
// 標題為"DC數位相機 - 露天拍賣"?
this.assert.equal(title, 'DC數位相機 - 露天拍賣');
})
.isVisible('.header') // 確認 .header 可見?
Nightwatch Commands (cont.)
getAttribute, getTagName, getText
.getAttribute('#search_input', 'name', function(result) {
// 取得元素 #search_input 的屬性 name 的資料,並比對其值是否為 "k"
this.assert.equal(result.value, 'k');
})
.getTagName('#search_input', function(result) {
// 取得元素 #search_input 的 tag name 是否為 "input"
this.assert.equal(result.value, 'input');
})
.getText('.button', result => {
// 取得元素 .button 的文字,並比對是否為 "搜尋"
this.assert.equal(result.value, '搜尋')
})
Nightwatch Commands (cont.)
getCssProperty, getElementSize
.getCssProperty('#search_input', 'line-height', function(result) {
// 取得 #search_input 的 CSS line-height 的值,並比對是否為 "27px"
this.assert.equal(result.value, '27px');
})
.getElementSize('.submit', result => {
// 取得元素 .submit 的寬高
// 比對其寬是否為 75px,其高是否為 27px
this.assert.equal(result.value.width, 75);
this.assert.equal(result.value.height, 27);
})
Nightwatch Commands (cont.)
clearValue, getValue
.clearValue('#search_input') // 清除 #search_input 的值
.setValue('#search_input', 'Pusheen') // 輸入 "Pusheen"
.getValue("#search_input", function(result) {
// 取得 #search_input 欄位值,並比對其值是否為 "Pusheen"
this.assert.equal(result.value, 'Pusheen');
})
.click('.submit') // 點擊送出按鈕
.end() // 結束 session,關閉瀏覽器
}
nightwatch test/e2e/class/testSubCategory.js
Nightwatch Commands (cont.)
saveScreenshot
'Test Save Screenshot': browser => {
browser
.rtUrl('www')
.maximizeWindow() //展開到螢幕到最大寬度
.saveScreenshot('./screenshots/index.png') //儲存螢幕截圖
.end()
}
nightwatch test/e2e/testSaveScreenshot.js
BDD
BDD(Behavior-Driven Development,行為驅動開發)意即在開發前先撰寫測試程
式,以確保程式碼品質符合驗收規格。除了實作前先寫測試外,還要寫一份「可以
執行的規格」。白話文就是使用者想看到什麼、打開什麼、點到什麼,就這麼寫在測
試程式裡面。
● 進入首頁即可看到一個紅色的按鈕(O)
● 依序點擊按鈕「1」、「+」、「2」、「=」,輸入框裡的值為「3」(O)
● 函式 add(1, 2) 回傳得到 3(X,並非以使用者可執行的角度撰寫規格)
BDD vs TDD
BDD 其實是一種 TDD,最大的差異在於
● BDD:從使用者的角度去思考驗收規格
● TDD:從測試結果去思考程式該如何實作
斷言
● 判斷預期和實際的狀況,不如預期就報錯
● Nightwatch 的斷言有兩種
○ Expect
○ Assert / Verify
Expect
● Nightwatch 的 BDD Expect 是源自於 Chai 的 Expect API
○ expect 比 assert 更有彈性和口語化
○ 只能用於網頁元素的比對
○ 缺點:不能串起來(chain)使用
● Chainable Getters:連接元素和斷言
○ to、be、been、is、that、which、and、has、have、with、at、does、of
browser.expect.element('.heading').text.to.equal('露天旗艦店');
browser.expect.element('.heading').text.be.equal('露天旗艦店');
Expect
text, equals, contains, matches, not
'Test Main Category Page 1': browser => {
browser.url('http://class.ruten.com.tw/category/main?0008');
// 文字內容不為 Hello World?
browser.expect.element('.breadcrumb').text.to.not.equals('Hello World');
// 文字內容包含「攝影機」?
browser.expect.element('.breadcrumb').text.to.contains('攝影機');
// 文字內容不含數字?
browser.expect.element('.breadcrumb').text.to.matches(/^([^0-9]*)$/);
browser.end();
}
nightwatch test/e2e/class/testMainCategorySimpleExpect1.js
Expect
a / an, css, attribute, present, visible
'Test Main Category Page 2': browser => {
browser.url('http://class.ruten.com.tw/category/main?0008');
// #search 是一個 input?
browser.expect.element('#search').to.be.an('input', '#search 必須為 input');
// CSS 屬性 display 的值是 inline-block?
browser.expect.element('.ad-item').css('display').to.equals('inline-block');
browser.expect.element('.rt-ad-link').attribute('href'); // 含有屬性 href?
browser.expect.element('#ad-flash').to.be.visible; // 可見?
browser.expect.element('.shopping-mall').to.be.present; // 存在?
browser.end();
}
nightwatch test/e2e/class/testMainCategorySimpleExpect2.js
Expect
enabled, selected, value
'Test Find Pusheen Page': browser => {
browser.url('https://find.ruten.com.tw...');
browser.expect.element('.payment').to.be.selected; // 選取?
browser.expect.element('.input').to.have.value
.that.equals('pusheen'); // 值為 pusheen?
browser.expect.element('.button').to.be.enabled; // 啟用?
browser.end();
}
nightwatch test/e2e/find/testFindPusheenSimpleExpect.js
Assert
title, containsText, value, valueContains
'Test Main Category Page': browser => {
browser
.url('http://class.ruten.com.tw/category/main?0008')
.assert.title('相機、攝影機 - 露天拍賣') // title 等於特定字串?
.setValue('#search_input', '好吃的蛋糕')
.assert.value('#search_input', '好吃的蛋糕') // 表單元件的值等於「好吃的蛋糕」?
.assert.valueContains('#search_input', '蛋糕') // 表單元件的值包含「蛋糕」?
.assert.containsText('.submit', '再搜尋') // 文字節點內容包含「再搜尋」?
.end();
}
nightwatch test/e2e/class/testMainCategoryAssertSimpleExample1.js
Assert (cont.)
urlEquals, urlContains, visible, elementPresent
'Test Main Category Page': browser => {
browser
.url('http://class.ruten.com.tw/category/main?0008')
// 目前網址等於特定字串?
.assert.urlEquals('http://class.ruten.com.tw/category/main?0008')
.assert.urlContains('/category/main') // 目前網址包含特定字串?
.assert.visible('#ad-flash') // 可見?
.assert.elementPresent('.top-sell') // 存在?
.end();
}
nightwatch test/e2e/class/testMainCategorySimpleExpect2.js
Assert (cont.)
attributeEquals, attributeContains, cssProperty, cssClassPresent
'Test Main Category Page': browser => {
browser
.url('http://class.ruten.com.tw/category/main?0008')
// 屬性 type 為 submit?
.assert.attributeEquals('.submit', 'type', 'submit')
// 檢視 class 包含 button?
.assert.attributeContains('.submit', 'class', 'button')
// CSS 屬性等於指定的值?
.assert.cssProperty('.submit', 'min-height', '24px')
.assert.cssClassPresent('.submit', 'button') // 含有 CSS class?
.end();
}
nightwatch test/e2e/class/testMainCategorySimpleExpect3.js
Visible vs Present
DOM Element 的可見與存在
● visible:可見,必為存在
● hidden:隱藏(display: none / opacity: 0)
● elementPresent:存在
● elementNotPresent:不存在
猜猜看!
'Guess visible or present?': browser => {
browser
.rtUrl('class', 'category/main?0023')
.verify.visible('.header')
.verify.elementPresent('.header')
.verify.hidden('.block')
.verify.elementPresent('.block')
.verify.elementNotPresent('.abcdef')
.end()
}
.header
.block (display: none)
猜猜看!(cont.)
'Guess visible or present?': browser => {
browser
.rtUrl('class', 'category/main?0023')
.verify.visible('.header') // (O) 可見
.verify.elementPresent('.header') // (O) 可見,必為存在
.verify.hidden('.block') // (O) 不可見,隱藏
.verify.elementPresent('.block') // (O) 存在,但隱藏
.verify.elementNotPresent('.abcdef') // (O) 不存在
.end()
}
nightwatch test/e2e/class/testVisibleOrPresent.js
Assert vs Verify
斷言失敗時的處理方式
Assert:忽略剩餘未執行的部份 Verify:繼續未執行的部份
Test Hooks
Test Hooks
before: browser => {
// Test Suite 開始前執行
},
after: browser => {
// Test Suite 結束後執行
},
beforeEach: browser => {
// 在 Test Case 開始前執行
},
afterEach: (browser, done) => {
// 在 Test Case 結束後執行
done();
}
會印出什麼?
module.exports = {
before: browser => {
console.log('abc');
},
after: browser => {
console.log('def');
},
beforeEach: browser => {
console.log('xyz');
},
afterEach: (browser, done) => {
console.log('012');
done();
},
'Test 123': browser => {
// ...
},
'Test 456': browser => {
// ...
}
}
會印出什麼?答案是...
執行這個 Test Suite,結果如下
abc
xyz
012
xyz
012
def
Nightwatch Test Runner
● 分組、標籤、禁跑特定測試
分組測試
分組的方式就是將測試程式碼放進同一個資料夾,群組名稱即資料夾名稱
● 依照分類跑測試
○ nightwatch --group [group_name1,group_name2]
○ 簡寫 nightwatch -g [group_name1,group_name2]
○ EX: nightwatch --group www
● 依照分類忽略測試
○ nightwatch --skipgroup [group_name1,group_name2]
分組測試 (cont.)
tests/e2e
├── class
| ├── testMainCategory.js (O,執行)
| └── testSubCategory.js (O,執行)
├── point
| ├── test1111.js (O,執行)
| └── testHotTopics.js (O,執行)
└── testDemo.js (X,不執行)
執行 nightwatch --group class,point,只會執行 4 個檔案
依標籤測試
Nightwatch 允許開發者使用標籤(tag)標記測試程式
● 依照標籤跑測試
○ nightwatch --tag [tag_name_1] --tag [tag_name_2]
● 依照標籤忽略測試
○ nightwatch --skiptags [tag_name_1,tag_name_2]
標籤的好處是有彈性。
一個 Test Suite 可有多個不同的標籤,不必受限於分類的唯一和垂直特性
依標籤測試 (cont.)
module.exports = {
'@tags': ['campaign', 'point'], // Test Suite 可設定標籤
'Demo Ruten Campaign 1111 Page': browser => {
browser
.url('http://pub.ruten.com.tw/20171111/index.html')
.end()
}
}
依標籤測試 (cont.)
tests/e2e
├── class
| ├── testMainCategory.js (class) (X,不執行)
| └── testSubCategory.js (class) (X,不執行)
├── point
| ├── test1111.js (campaign, point) (O,執行)
| └── testHotTopics.js (point) (O,執行)
└── testDemo.js (index) (X,不執行)
執行 nightwatch --tag point,只會執行 2 個檔案
禁跑特定測試
禁跑特定 Test Suite:設定 @disabled 為 true
module.exports = {
'@disabled': true,
'Demo Google Page': browser => {
browser
.url('https://www.google.com.tw/')
.end()
}
}
禁跑特定測試
禁跑特定 Test Case:在 Test Case 前加上一個空字串
module.exports = {
'sample test': function (client) { // 會跑這個 Test Case
// ...
},
'other sample test': '' + function (client) {
// 加上空字串,禁跑特定 Test Case
}
};
測試程式的模組化
● Page Object
● 客製化指令
● 客製化斷言
Page Objects
const commandList = {
submit: function() { /* ... */ }
}
module.exports = {
url: 'http://sample.com.tw',
commands: [commandList], // 指令
sections: { // 區塊, 作為 Namespacing
someSection: {
selector: '.some-selection',
elements: { // 元素
someElement: {
selector: '.some-element'
}
}
}
}
}
Page Objects (cont.)
module.exports = {
'Find Pusheen': browser => {
const findPage = browser.page.findPage(); // 使用 page object
findPage.navigate()
.assert.title('搜尋結果 : Pusheen - 露天拍賣')
.setValue('@searchbox', 'Pusheen')
.click('@submit');
browser.end();
}
};
nightwatch test/e2e/find/findPusheen.js
客製化指令(Custom Commands)
● 設定檔案路徑
○ 在 nightwatch.conf.js 的 custom_commands_path 設定檔案路徑
● 檔名即指令名稱
● 範例:登入露天桌機版網站
○ nightwatch test/e2e/member/testRutenLogin.js
客製化指令 (cont.)
'Login Ruten Desktop Website': browser => {
browser
.url('https://member.ruten.com.tw/user/login.htm')
.setValue('.user', 'nightwatch101')
.setValue('.password', '*****')
.click('.submit')
.end();
}
客製化指令 (cont.)
exports.command = function(user = 'nightwatch101') {
const accounts = require('../settings.js').accounts;
this
.rtUrl('member', 'login.php')
.setValue('.user', user)
.setValue('.password', accounts[user].password)
.click('.submit')
return this;
};
客製化指令 (cont.)
'Login Ruten Desktop Website': browser => {
browser
.rtLogin() // 乾淨易懂可重用!
.end();
}
客製化斷言(Custom Assertions)
● 用於擴充 Assert 和 Verify
● 檔案路徑:在 nightwatch.conf.js 的 custom_assertions_path 設定檔案路徑
● 檔名即指令名稱
● 格式
○ message:測試報告顯示的訊息( ✓ Testing if the element <div> has count: 13)
○ expected:期待比對的值
○ pass:實際進行斷言的地方
○ value:實際狀況的值,會被 pass 當參數傳入使用
○ command:執行瀏覽器指令的地方,執行結果會當成參數回傳給 value,再執行 pass
客製化斷言 (cont.)
範例:判斷網頁元素數目是否等於預期數量
exports.assertion = function (selector, count) {
this.message = `Testing if the element <${selector}> has count: ${count}`;
this.expected = count;
this.pass = function(val) { return val === this.expected; }
this.value = function(res) { return res; }
this.command = function(cb) {
return this.api.execute(function(selector) {
return document.querySelectorAll(selector).length;
}, [selector], function(res) { cb.call(this, res.value); }.bind(this));
}
}
測試報告 - 難以閱讀的 XML 格式
<?xml version="1.0" encoding="UTF-8" ?>
<testsuites errors="0" failures="0" tests="1">
<testsuite name="class.testMainCategory" errors="0" failures="0" hostname=""
id="" package="class" skipped="0" tests="1" time="20.28" timestamp="Fri, 01 Dec
2017 12:06:52 GMT">
<testcase name="Demo Ruten MainCategory Page"
classname="class.testMainCategory" time="20.28" assertions="0"></testcase>
</testsuite>
</testsuites>
客製化測試報告 - nightwatch-html-reporter
客製化測試報告 - nightwatch-html-reporter
客製化測試報告 (cont.)
HtmlReporter 可設定的選項
● openBrowser:跑完測試後所產生的報告是否使用瀏覽器打開
● reportsDirectory:測試報告的所在路徑
● reportFilename:測試報告的檔名,預設是 generatedReport.html
● uniqueFilename:測試報告是否要加上 timestamp
● separateReportPerSuite:測試報告是否要加上 test suite 的名稱
● themeName:測試報告所使用的主題名稱
● hideSuccess:是否隱藏成功的測試案例,測試報告只顯示錯誤的部
● timestamprelativeScreenshots:是否將截圖的路徑設為相對路徑
範例 - 露天拍賣-桌機版購物車
登入 -> 購買商品 -> 購物車 -> 結帳
● 檢視是否登入露天拍賣,若沒有登入就登入
● 商品頁:點擊按鈕「馬上購買」將商品加入購物車
● 購車頁:點擊按鈕「確定購買」
● 結帳頁:選擇運送方式「面交取貨」、填寫取件人資料
● 回首頁,登出
nightwatch test/e2e/mybid/testShoppingCart.js
希望露天拍賣網站功能都能完善!
總結
● End-to-End Testing vs Unit Testing
● 小叮嚀
● QnA
End-to-End Testing vs Unit Testing
● Unit Testing 的主體是程式碼,是測試程式碼的自身行為,也就是驗證輸入與
輸出是否符合預期。
● End-to-End Testing 的主體是使用者,是一種模擬用戶行為的 UI 測試或進行
流程上的整合測試。
小叮嚀
● Test Suite 無特定執行順序,彼此不可相依
● 不要使用 pause 做等待,改用 waitForElementVisible
○ 因為等待時間可能會因網路速度延遲而導致無法固定等待時間
● 有驗證才知道成功或失敗
○ 單純使用指令 isVisible 而不搭配斷言,無法知道成功或失敗
● 如何優化測試程式?
○ 將常用的功能抽出來成為模組,成為客製化指令和斷言
○ 使用 Page Object 封裝網頁片段,增加重用和可讀性,減少維護的複雜度
QnA
● 寫測試是否會增加額外工時?
個人經驗是增加一倍。
● 除了程式碼的品質保證外,還有什麼好處?
記錄規格、方便估時程。
○ 測試案例如同告知開發者規格的細節和範例,再也不怕同事離職,無人可問。
○ 魔鬼藏在細節裡,測試程式會告訴我們功能有多細;而且網站久了很容易有地雷,這讓我們
記得把踩雷時間估進去 (〒︿〒)

Nightwatch101

  • 1.
  • 2.
    Agenda ● Nightwatch 與Selenium Webdriver ● 環境建置 ● 設定檔 ● 定位網頁元素:CSS Selector 與 Xpath ● Nightwatch Commands ● 斷言:BDD Expect、Assert、Verify
  • 3.
    Agenda (cont.) ● TestHooks ● Nightwatch Test Runner:分組、標籤、禁跑特定測試 ● Page Objects ● 客製化指令與斷言 ● 客製化測試報告 ● 總結
  • 4.
  • 5.
  • 6.
  • 7.
    Nightwatch 簡介 ● Nightwatch與 Selenium Webdriver ● 環境建置 ● 設定檔
  • 8.
    Nightwatch Nightwatch 是什麼? ● 網頁專用的自動化測試框架 End-to-EndTesting 是什麼? ● 模擬使用者對瀏覽器進行操作,例如:瀏覽網址、輸入文字、點擊按鈕 ● 可做 UI 測試、整合測試
  • 9.
  • 10.
    環境建置 ● 安裝 JavaDevelopment Kit(JDK),版本 7+ ● 安裝 Nightwatch ● 下載專案 https://goo.gl/mFHJ2c ● 使用 Test Runner 進行測試 ○ nightwatch ./test/e2e/testDemo.js
  • 11.
    Nightwatch Test Runner設定檔 Nightwatch 提供了 Command-line Test Runner,用來跑各種類型的測試 例如:指定測試環境、依群組或標籤或個別檔案 ● 設定檔在專案根目錄下 ● 預設檔名是 nightwatch.json 或 nightwatch.conf.js(優先) ● 設定檔分為三個部分:基本設定(Page Objects、客製化指令和斷言的位置)、 Selenium Server 相關、測試環境相關 ● External Globals:放置複雜的運算或 Plugin 範例
  • 12.
    Nightwatch 指令與斷言 ● 定位網頁元素:CSSSelector 與 Xpath ● 常用指令 ● 斷言:BDD Expect、Assert、Verify
  • 13.
  • 14.
    使用 CSS Selector定位網頁元素 .waitForElementVisible('body', 1000) //等待 1 秒,確認 <body> 是否出現 .isVisible('.header') //確認 ".header" 是否可見 .getText('.link') //取得 ".link" 內的文字 .setValue('.input', 'Pusheen') //".input" 欄位輸入 "Pusheen"
  • 15.
    使用 Xpath 定位網頁元素 ●設定檔 use_xpath: true 可設定使用 Xpath 為預設選取策略 ● 切換使用 CSS Selector 或 Xpath .useXpath() //使用 Xpath 來抓取網頁元素 .useCss() //使用 CSS Selector 來抓取網頁元素 //在帳號欄位輸入字串 nightwatch101 .setValue('//input[@id="userid"]', 'nightwatch101') //在密碼欄位輸入字串 nightwatch101 .setValue('//input[@id="userpass"]', 'nightwatch101')
  • 16.
    Nightwatch Commands url, waitForElementPresent,setValue, click, pause, end 'Ruten Desktop Login': browser => { browser .url('https://member.ruten.com.tw/user/login.htm') // 打開指定網址 .waitForElementPresent('body') // 存在? 確認 DOM Element 載入完成 .setValue('#userid', 'nightwatch101') // 對 input text 鍵入字串 .setValue('#userpass', '*************') .click('#btnLogin') // 點擊送出按鈕 .pause(1000) // 暫停測試程式,可指定暫停時間( ms) .end(); // 結束 session,關閉瀏覽器 } nightwatch test/e2e/member/testDesktopLogin.js
  • 17.
    Nightwatch Commands (cont.) waitForElementVisible,getTitle, isVisible 'Demo Ruten SubCategory Page': browser => { browser .url('http://class.ruten.com.tw/category/sub00.php?c=00080001') .waitForElementVisible('body') // <body> 可見? .getTitle(function(title) { // 取得網頁標題 // 標題為"DC數位相機 - 露天拍賣"? this.assert.equal(title, 'DC數位相機 - 露天拍賣'); }) .isVisible('.header') // 確認 .header 可見?
  • 18.
    Nightwatch Commands (cont.) getAttribute,getTagName, getText .getAttribute('#search_input', 'name', function(result) { // 取得元素 #search_input 的屬性 name 的資料,並比對其值是否為 "k" this.assert.equal(result.value, 'k'); }) .getTagName('#search_input', function(result) { // 取得元素 #search_input 的 tag name 是否為 "input" this.assert.equal(result.value, 'input'); }) .getText('.button', result => { // 取得元素 .button 的文字,並比對是否為 "搜尋" this.assert.equal(result.value, '搜尋') })
  • 19.
    Nightwatch Commands (cont.) getCssProperty,getElementSize .getCssProperty('#search_input', 'line-height', function(result) { // 取得 #search_input 的 CSS line-height 的值,並比對是否為 "27px" this.assert.equal(result.value, '27px'); }) .getElementSize('.submit', result => { // 取得元素 .submit 的寬高 // 比對其寬是否為 75px,其高是否為 27px this.assert.equal(result.value.width, 75); this.assert.equal(result.value.height, 27); })
  • 20.
    Nightwatch Commands (cont.) clearValue,getValue .clearValue('#search_input') // 清除 #search_input 的值 .setValue('#search_input', 'Pusheen') // 輸入 "Pusheen" .getValue("#search_input", function(result) { // 取得 #search_input 欄位值,並比對其值是否為 "Pusheen" this.assert.equal(result.value, 'Pusheen'); }) .click('.submit') // 點擊送出按鈕 .end() // 結束 session,關閉瀏覽器 } nightwatch test/e2e/class/testSubCategory.js
  • 21.
    Nightwatch Commands (cont.) saveScreenshot 'TestSave Screenshot': browser => { browser .rtUrl('www') .maximizeWindow() //展開到螢幕到最大寬度 .saveScreenshot('./screenshots/index.png') //儲存螢幕截圖 .end() } nightwatch test/e2e/testSaveScreenshot.js
  • 22.
  • 23.
    BDD vs TDD BDD其實是一種 TDD,最大的差異在於 ● BDD:從使用者的角度去思考驗收規格 ● TDD:從測試結果去思考程式該如何實作
  • 24.
  • 25.
    Expect ● Nightwatch 的BDD Expect 是源自於 Chai 的 Expect API ○ expect 比 assert 更有彈性和口語化 ○ 只能用於網頁元素的比對 ○ 缺點:不能串起來(chain)使用 ● Chainable Getters:連接元素和斷言 ○ to、be、been、is、that、which、and、has、have、with、at、does、of browser.expect.element('.heading').text.to.equal('露天旗艦店'); browser.expect.element('.heading').text.be.equal('露天旗艦店');
  • 26.
    Expect text, equals, contains,matches, not 'Test Main Category Page 1': browser => { browser.url('http://class.ruten.com.tw/category/main?0008'); // 文字內容不為 Hello World? browser.expect.element('.breadcrumb').text.to.not.equals('Hello World'); // 文字內容包含「攝影機」? browser.expect.element('.breadcrumb').text.to.contains('攝影機'); // 文字內容不含數字? browser.expect.element('.breadcrumb').text.to.matches(/^([^0-9]*)$/); browser.end(); } nightwatch test/e2e/class/testMainCategorySimpleExpect1.js
  • 27.
    Expect a / an,css, attribute, present, visible 'Test Main Category Page 2': browser => { browser.url('http://class.ruten.com.tw/category/main?0008'); // #search 是一個 input? browser.expect.element('#search').to.be.an('input', '#search 必須為 input'); // CSS 屬性 display 的值是 inline-block? browser.expect.element('.ad-item').css('display').to.equals('inline-block'); browser.expect.element('.rt-ad-link').attribute('href'); // 含有屬性 href? browser.expect.element('#ad-flash').to.be.visible; // 可見? browser.expect.element('.shopping-mall').to.be.present; // 存在? browser.end(); } nightwatch test/e2e/class/testMainCategorySimpleExpect2.js
  • 28.
    Expect enabled, selected, value 'TestFind Pusheen Page': browser => { browser.url('https://find.ruten.com.tw...'); browser.expect.element('.payment').to.be.selected; // 選取? browser.expect.element('.input').to.have.value .that.equals('pusheen'); // 值為 pusheen? browser.expect.element('.button').to.be.enabled; // 啟用? browser.end(); } nightwatch test/e2e/find/testFindPusheenSimpleExpect.js
  • 29.
    Assert title, containsText, value,valueContains 'Test Main Category Page': browser => { browser .url('http://class.ruten.com.tw/category/main?0008') .assert.title('相機、攝影機 - 露天拍賣') // title 等於特定字串? .setValue('#search_input', '好吃的蛋糕') .assert.value('#search_input', '好吃的蛋糕') // 表單元件的值等於「好吃的蛋糕」? .assert.valueContains('#search_input', '蛋糕') // 表單元件的值包含「蛋糕」? .assert.containsText('.submit', '再搜尋') // 文字節點內容包含「再搜尋」? .end(); } nightwatch test/e2e/class/testMainCategoryAssertSimpleExample1.js
  • 30.
    Assert (cont.) urlEquals, urlContains,visible, elementPresent 'Test Main Category Page': browser => { browser .url('http://class.ruten.com.tw/category/main?0008') // 目前網址等於特定字串? .assert.urlEquals('http://class.ruten.com.tw/category/main?0008') .assert.urlContains('/category/main') // 目前網址包含特定字串? .assert.visible('#ad-flash') // 可見? .assert.elementPresent('.top-sell') // 存在? .end(); } nightwatch test/e2e/class/testMainCategorySimpleExpect2.js
  • 31.
    Assert (cont.) attributeEquals, attributeContains,cssProperty, cssClassPresent 'Test Main Category Page': browser => { browser .url('http://class.ruten.com.tw/category/main?0008') // 屬性 type 為 submit? .assert.attributeEquals('.submit', 'type', 'submit') // 檢視 class 包含 button? .assert.attributeContains('.submit', 'class', 'button') // CSS 屬性等於指定的值? .assert.cssProperty('.submit', 'min-height', '24px') .assert.cssClassPresent('.submit', 'button') // 含有 CSS class? .end(); } nightwatch test/e2e/class/testMainCategorySimpleExpect3.js
  • 32.
    Visible vs Present DOMElement 的可見與存在 ● visible:可見,必為存在 ● hidden:隱藏(display: none / opacity: 0) ● elementPresent:存在 ● elementNotPresent:不存在
  • 33.
    猜猜看! 'Guess visible orpresent?': browser => { browser .rtUrl('class', 'category/main?0023') .verify.visible('.header') .verify.elementPresent('.header') .verify.hidden('.block') .verify.elementPresent('.block') .verify.elementNotPresent('.abcdef') .end() } .header .block (display: none)
  • 34.
    猜猜看!(cont.) 'Guess visible orpresent?': browser => { browser .rtUrl('class', 'category/main?0023') .verify.visible('.header') // (O) 可見 .verify.elementPresent('.header') // (O) 可見,必為存在 .verify.hidden('.block') // (O) 不可見,隱藏 .verify.elementPresent('.block') // (O) 存在,但隱藏 .verify.elementNotPresent('.abcdef') // (O) 不存在 .end() } nightwatch test/e2e/class/testVisibleOrPresent.js
  • 35.
  • 36.
  • 37.
    Test Hooks before: browser=> { // Test Suite 開始前執行 }, after: browser => { // Test Suite 結束後執行 }, beforeEach: browser => { // 在 Test Case 開始前執行 }, afterEach: (browser, done) => { // 在 Test Case 結束後執行 done(); }
  • 38.
    會印出什麼? module.exports = { before:browser => { console.log('abc'); }, after: browser => { console.log('def'); }, beforeEach: browser => { console.log('xyz'); }, afterEach: (browser, done) => { console.log('012'); done(); }, 'Test 123': browser => { // ... }, 'Test 456': browser => { // ... } }
  • 39.
  • 40.
    Nightwatch Test Runner ●分組、標籤、禁跑特定測試
  • 41.
    分組測試 分組的方式就是將測試程式碼放進同一個資料夾,群組名稱即資料夾名稱 ● 依照分類跑測試 ○ nightwatch--group [group_name1,group_name2] ○ 簡寫 nightwatch -g [group_name1,group_name2] ○ EX: nightwatch --group www ● 依照分類忽略測試 ○ nightwatch --skipgroup [group_name1,group_name2]
  • 42.
    分組測試 (cont.) tests/e2e ├── class |├── testMainCategory.js (O,執行) | └── testSubCategory.js (O,執行) ├── point | ├── test1111.js (O,執行) | └── testHotTopics.js (O,執行) └── testDemo.js (X,不執行) 執行 nightwatch --group class,point,只會執行 4 個檔案
  • 43.
    依標籤測試 Nightwatch 允許開發者使用標籤(tag)標記測試程式 ● 依照標籤跑測試 ○nightwatch --tag [tag_name_1] --tag [tag_name_2] ● 依照標籤忽略測試 ○ nightwatch --skiptags [tag_name_1,tag_name_2] 標籤的好處是有彈性。 一個 Test Suite 可有多個不同的標籤,不必受限於分類的唯一和垂直特性
  • 44.
    依標籤測試 (cont.) module.exports ={ '@tags': ['campaign', 'point'], // Test Suite 可設定標籤 'Demo Ruten Campaign 1111 Page': browser => { browser .url('http://pub.ruten.com.tw/20171111/index.html') .end() } }
  • 45.
    依標籤測試 (cont.) tests/e2e ├── class |├── testMainCategory.js (class) (X,不執行) | └── testSubCategory.js (class) (X,不執行) ├── point | ├── test1111.js (campaign, point) (O,執行) | └── testHotTopics.js (point) (O,執行) └── testDemo.js (index) (X,不執行) 執行 nightwatch --tag point,只會執行 2 個檔案
  • 46.
    禁跑特定測試 禁跑特定 Test Suite:設定@disabled 為 true module.exports = { '@disabled': true, 'Demo Google Page': browser => { browser .url('https://www.google.com.tw/') .end() } }
  • 47.
    禁跑特定測試 禁跑特定 Test Case:在Test Case 前加上一個空字串 module.exports = { 'sample test': function (client) { // 會跑這個 Test Case // ... }, 'other sample test': '' + function (client) { // 加上空字串,禁跑特定 Test Case } };
  • 48.
    測試程式的模組化 ● Page Object ●客製化指令 ● 客製化斷言
  • 49.
    Page Objects const commandList= { submit: function() { /* ... */ } } module.exports = { url: 'http://sample.com.tw', commands: [commandList], // 指令 sections: { // 區塊, 作為 Namespacing someSection: { selector: '.some-selection', elements: { // 元素 someElement: { selector: '.some-element' } } } } }
  • 50.
    Page Objects (cont.) module.exports= { 'Find Pusheen': browser => { const findPage = browser.page.findPage(); // 使用 page object findPage.navigate() .assert.title('搜尋結果 : Pusheen - 露天拍賣') .setValue('@searchbox', 'Pusheen') .click('@submit'); browser.end(); } }; nightwatch test/e2e/find/findPusheen.js
  • 51.
    客製化指令(Custom Commands) ● 設定檔案路徑 ○在 nightwatch.conf.js 的 custom_commands_path 設定檔案路徑 ● 檔名即指令名稱 ● 範例:登入露天桌機版網站 ○ nightwatch test/e2e/member/testRutenLogin.js
  • 52.
    客製化指令 (cont.) 'Login RutenDesktop Website': browser => { browser .url('https://member.ruten.com.tw/user/login.htm') .setValue('.user', 'nightwatch101') .setValue('.password', '*****') .click('.submit') .end(); }
  • 53.
    客製化指令 (cont.) exports.command =function(user = 'nightwatch101') { const accounts = require('../settings.js').accounts; this .rtUrl('member', 'login.php') .setValue('.user', user) .setValue('.password', accounts[user].password) .click('.submit') return this; };
  • 54.
    客製化指令 (cont.) 'Login RutenDesktop Website': browser => { browser .rtLogin() // 乾淨易懂可重用! .end(); }
  • 55.
    客製化斷言(Custom Assertions) ● 用於擴充Assert 和 Verify ● 檔案路徑:在 nightwatch.conf.js 的 custom_assertions_path 設定檔案路徑 ● 檔名即指令名稱 ● 格式 ○ message:測試報告顯示的訊息( ✓ Testing if the element <div> has count: 13) ○ expected:期待比對的值 ○ pass:實際進行斷言的地方 ○ value:實際狀況的值,會被 pass 當參數傳入使用 ○ command:執行瀏覽器指令的地方,執行結果會當成參數回傳給 value,再執行 pass
  • 56.
    客製化斷言 (cont.) 範例:判斷網頁元素數目是否等於預期數量 exports.assertion =function (selector, count) { this.message = `Testing if the element <${selector}> has count: ${count}`; this.expected = count; this.pass = function(val) { return val === this.expected; } this.value = function(res) { return res; } this.command = function(cb) { return this.api.execute(function(selector) { return document.querySelectorAll(selector).length; }, [selector], function(res) { cb.call(this, res.value); }.bind(this)); } }
  • 57.
    測試報告 - 難以閱讀的XML 格式 <?xml version="1.0" encoding="UTF-8" ?> <testsuites errors="0" failures="0" tests="1"> <testsuite name="class.testMainCategory" errors="0" failures="0" hostname="" id="" package="class" skipped="0" tests="1" time="20.28" timestamp="Fri, 01 Dec 2017 12:06:52 GMT"> <testcase name="Demo Ruten MainCategory Page" classname="class.testMainCategory" time="20.28" assertions="0"></testcase> </testsuite> </testsuites>
  • 58.
  • 59.
  • 60.
    客製化測試報告 (cont.) HtmlReporter 可設定的選項 ●openBrowser:跑完測試後所產生的報告是否使用瀏覽器打開 ● reportsDirectory:測試報告的所在路徑 ● reportFilename:測試報告的檔名,預設是 generatedReport.html ● uniqueFilename:測試報告是否要加上 timestamp ● separateReportPerSuite:測試報告是否要加上 test suite 的名稱 ● themeName:測試報告所使用的主題名稱 ● hideSuccess:是否隱藏成功的測試案例,測試報告只顯示錯誤的部 ● timestamprelativeScreenshots:是否將截圖的路徑設為相對路徑
  • 61.
    範例 - 露天拍賣-桌機版購物車 登入-> 購買商品 -> 購物車 -> 結帳 ● 檢視是否登入露天拍賣,若沒有登入就登入 ● 商品頁:點擊按鈕「馬上購買」將商品加入購物車 ● 購車頁:點擊按鈕「確定購買」 ● 結帳頁:選擇運送方式「面交取貨」、填寫取件人資料 ● 回首頁,登出 nightwatch test/e2e/mybid/testShoppingCart.js
  • 62.
  • 63.
    總結 ● End-to-End Testingvs Unit Testing ● 小叮嚀 ● QnA
  • 64.
    End-to-End Testing vsUnit Testing ● Unit Testing 的主體是程式碼,是測試程式碼的自身行為,也就是驗證輸入與 輸出是否符合預期。 ● End-to-End Testing 的主體是使用者,是一種模擬用戶行為的 UI 測試或進行 流程上的整合測試。
  • 65.
    小叮嚀 ● Test Suite無特定執行順序,彼此不可相依 ● 不要使用 pause 做等待,改用 waitForElementVisible ○ 因為等待時間可能會因網路速度延遲而導致無法固定等待時間 ● 有驗證才知道成功或失敗 ○ 單純使用指令 isVisible 而不搭配斷言,無法知道成功或失敗 ● 如何優化測試程式? ○ 將常用的功能抽出來成為模組,成為客製化指令和斷言 ○ 使用 Page Object 封裝網頁片段,增加重用和可讀性,減少維護的複雜度
  • 66.
    QnA ● 寫測試是否會增加額外工時? 個人經驗是增加一倍。 ● 除了程式碼的品質保證外,還有什麼好處? 記錄規格、方便估時程。 ○測試案例如同告知開發者規格的細節和範例,再也不怕同事離職,無人可問。 ○ 魔鬼藏在細節裡,測試程式會告訴我們功能有多細;而且網站久了很容易有地雷,這讓我們 記得把踩雷時間估進去 (〒︿〒)