O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.

Nightwatch101

2.877 visualizações

Publicada em

手牽手一起來學 Nightwatch!

Publicada em: Engenharia
  • Seja o primeiro a comentar

Nightwatch101

  1. 1. Nightwatch101 手牽手一起來學 Nightwatch!
  2. 2. Agenda ● Nightwatch 與 Selenium Webdriver ● 環境建置 ● 設定檔 ● 定位網頁元素:CSS Selector 與 Xpath ● Nightwatch Commands ● 斷言:BDD Expect、Assert、Verify
  3. 3. Agenda (cont.) ● Test Hooks ● Nightwatch Test Runner:分組、標籤、禁跑特定測試 ● Page Objects ● 客製化指令與斷言 ● 客製化測試報告 ● 總結
  4. 4. 露天拍賣。前端工程師 cythilya@gmail.com https://cythilya.github.io @cythilya Summer
  5. 5. 網站又壞了? 結帳折扣金 額有誤? 上架頁 不能改規格 首頁廣告 無法顯示 搜尋結果 出不來 XD
  6. 6. 人工測試太辛苦了, 用 Nightwatch 自動處理吧
  7. 7. Nightwatch 簡介 ● Nightwatch 與 Selenium Webdriver ● 環境建置 ● 設定檔
  8. 8. Nightwatch Nightwatch 是什麼? ● 網頁專用的自動化測試框架 End-to-End Testing 是什麼? ● 模擬使用者對瀏覽器進行操作,例如:瀏覽網址、輸入文字、點擊按鈕 ● 可做 UI 測試、整合測試
  9. 9. Nightwatch 與 Selenium Webdriver
  10. 10. 環境建置 ● 安裝 Java Development Kit(JDK),版本 7+ ● 安裝 Nightwatch ● 下載專案 https://goo.gl/mFHJ2c ● 使用 Test Runner 進行測試 ○ nightwatch ./test/e2e/testDemo.js
  11. 11. Nightwatch Test Runner 設定檔 Nightwatch 提供了 Command-line Test Runner,用來跑各種類型的測試 例如:指定測試環境、依群組或標籤或個別檔案 ● 設定檔在專案根目錄下 ● 預設檔名是 nightwatch.json 或 nightwatch.conf.js(優先) ● 設定檔分為三個部分:基本設定(Page Objects、客製化指令和斷言的位置)、 Selenium Server 相關、測試環境相關 ● External Globals:放置複雜的運算或 Plugin 範例
  12. 12. Nightwatch 指令與斷言 ● 定位網頁元素:CSS Selector 與 Xpath ● 常用指令 ● 斷言:BDD Expect、Assert、Verify
  13. 13. 定位網頁元素 定位網頁元素有兩種方法 ● CSS Selector ● Xpath
  14. 14. 使用 CSS Selector 定位網頁元素 .waitForElementVisible('body', 1000) //等待 1 秒,確認 <body> 是否出現 .isVisible('.header') //確認 ".header" 是否可見 .getText('.link') //取得 ".link" 內的文字 .setValue('.input', 'Pusheen') //".input" 欄位輸入 "Pusheen"
  15. 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. 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. 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. 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. 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. 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. 21. Nightwatch Commands (cont.) saveScreenshot 'Test Save Screenshot': browser => { browser .rtUrl('www') .maximizeWindow() //展開到螢幕到最大寬度 .saveScreenshot('./screenshots/index.png') //儲存螢幕截圖 .end() } nightwatch test/e2e/testSaveScreenshot.js
  22. 22. BDD BDD(Behavior-Driven Development,行為驅動開發)意即在開發前先撰寫測試程 式,以確保程式碼品質符合驗收規格。除了實作前先寫測試外,還要寫一份「可以 執行的規格」。白話文就是使用者想看到什麼、打開什麼、點到什麼,就這麼寫在測 試程式裡面。 ● 進入首頁即可看到一個紅色的按鈕(O) ● 依序點擊按鈕「1」、「+」、「2」、「=」,輸入框裡的值為「3」(O) ● 函式 add(1, 2) 回傳得到 3(X,並非以使用者可執行的角度撰寫規格)
  23. 23. BDD vs TDD BDD 其實是一種 TDD,最大的差異在於 ● BDD:從使用者的角度去思考驗收規格 ● TDD:從測試結果去思考程式該如何實作
  24. 24. 斷言 ● 判斷預期和實際的狀況,不如預期就報錯 ● Nightwatch 的斷言有兩種 ○ Expect ○ Assert / Verify
  25. 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. 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. 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. 28. 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
  29. 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. 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. 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. 32. Visible vs Present DOM Element 的可見與存在 ● visible:可見,必為存在 ● hidden:隱藏(display: none / opacity: 0) ● elementPresent:存在 ● elementNotPresent:不存在
  33. 33. 猜猜看! '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)
  34. 34. 猜猜看!(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
  35. 35. Assert vs Verify 斷言失敗時的處理方式 Assert:忽略剩餘未執行的部份 Verify:繼續未執行的部份
  36. 36. Test Hooks
  37. 37. Test Hooks before: browser => { // Test Suite 開始前執行 }, after: browser => { // Test Suite 結束後執行 }, beforeEach: browser => { // 在 Test Case 開始前執行 }, afterEach: (browser, done) => { // 在 Test Case 結束後執行 done(); }
  38. 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. 39. 會印出什麼?答案是... 執行這個 Test Suite,結果如下 abc xyz 012 xyz 012 def
  40. 40. Nightwatch Test Runner ● 分組、標籤、禁跑特定測試
  41. 41. 分組測試 分組的方式就是將測試程式碼放進同一個資料夾,群組名稱即資料夾名稱 ● 依照分類跑測試 ○ nightwatch --group [group_name1,group_name2] ○ 簡寫 nightwatch -g [group_name1,group_name2] ○ EX: nightwatch --group www ● 依照分類忽略測試 ○ nightwatch --skipgroup [group_name1,group_name2]
  42. 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. 43. 依標籤測試 Nightwatch 允許開發者使用標籤(tag)標記測試程式 ● 依照標籤跑測試 ○ nightwatch --tag [tag_name_1] --tag [tag_name_2] ● 依照標籤忽略測試 ○ nightwatch --skiptags [tag_name_1,tag_name_2] 標籤的好處是有彈性。 一個 Test Suite 可有多個不同的標籤,不必受限於分類的唯一和垂直特性
  44. 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. 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. 46. 禁跑特定測試 禁跑特定 Test Suite:設定 @disabled 為 true module.exports = { '@disabled': true, 'Demo Google Page': browser => { browser .url('https://www.google.com.tw/') .end() } }
  47. 47. 禁跑特定測試 禁跑特定 Test Case:在 Test Case 前加上一個空字串 module.exports = { 'sample test': function (client) { // 會跑這個 Test Case // ... }, 'other sample test': '' + function (client) { // 加上空字串,禁跑特定 Test Case } };
  48. 48. 測試程式的模組化 ● Page Object ● 客製化指令 ● 客製化斷言
  49. 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. 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. 51. 客製化指令(Custom Commands) ● 設定檔案路徑 ○ 在 nightwatch.conf.js 的 custom_commands_path 設定檔案路徑 ● 檔名即指令名稱 ● 範例:登入露天桌機版網站 ○ nightwatch test/e2e/member/testRutenLogin.js
  52. 52. 客製化指令 (cont.) 'Login Ruten Desktop Website': browser => { browser .url('https://member.ruten.com.tw/user/login.htm') .setValue('.user', 'nightwatch101') .setValue('.password', '*****') .click('.submit') .end(); }
  53. 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. 54. 客製化指令 (cont.) 'Login Ruten Desktop Website': browser => { browser .rtLogin() // 乾淨易懂可重用! .end(); }
  55. 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. 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. 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. 58. 客製化測試報告 - nightwatch-html-reporter
  59. 59. 客製化測試報告 - nightwatch-html-reporter
  60. 60. 客製化測試報告 (cont.) HtmlReporter 可設定的選項 ● openBrowser:跑完測試後所產生的報告是否使用瀏覽器打開 ● reportsDirectory:測試報告的所在路徑 ● reportFilename:測試報告的檔名,預設是 generatedReport.html ● uniqueFilename:測試報告是否要加上 timestamp ● separateReportPerSuite:測試報告是否要加上 test suite 的名稱 ● themeName:測試報告所使用的主題名稱 ● hideSuccess:是否隱藏成功的測試案例,測試報告只顯示錯誤的部 ● timestamprelativeScreenshots:是否將截圖的路徑設為相對路徑
  61. 61. 範例 - 露天拍賣-桌機版購物車 登入 -> 購買商品 -> 購物車 -> 結帳 ● 檢視是否登入露天拍賣,若沒有登入就登入 ● 商品頁:點擊按鈕「馬上購買」將商品加入購物車 ● 購車頁:點擊按鈕「確定購買」 ● 結帳頁:選擇運送方式「面交取貨」、填寫取件人資料 ● 回首頁,登出 nightwatch test/e2e/mybid/testShoppingCart.js
  62. 62. 希望露天拍賣網站功能都能完善!
  63. 63. 總結 ● End-to-End Testing vs Unit Testing ● 小叮嚀 ● QnA
  64. 64. End-to-End Testing vs Unit Testing ● Unit Testing 的主體是程式碼,是測試程式碼的自身行為,也就是驗證輸入與 輸出是否符合預期。 ● End-to-End Testing 的主體是使用者,是一種模擬用戶行為的 UI 測試或進行 流程上的整合測試。
  65. 65. 小叮嚀 ● Test Suite 無特定執行順序,彼此不可相依 ● 不要使用 pause 做等待,改用 waitForElementVisible ○ 因為等待時間可能會因網路速度延遲而導致無法固定等待時間 ● 有驗證才知道成功或失敗 ○ 單純使用指令 isVisible 而不搭配斷言,無法知道成功或失敗 ● 如何優化測試程式? ○ 將常用的功能抽出來成為模組,成為客製化指令和斷言 ○ 使用 Page Object 封裝網頁片段,增加重用和可讀性,減少維護的複雜度
  66. 66. QnA ● 寫測試是否會增加額外工時? 個人經驗是增加一倍。 ● 除了程式碼的品質保證外,還有什麼好處? 記錄規格、方便估時程。 ○ 測試案例如同告知開發者規格的細節和範例,再也不怕同事離職,無人可問。 ○ 魔鬼藏在細節裡,測試程式會告訴我們功能有多細;而且網站久了很容易有地雷,這讓我們 記得把踩雷時間估進去 (〒︿〒)

×