使用nightwatch進行E2E測試中文教程

任乃千發表於2017-06-29

E2E測試

E2E(end to end)測試是指端到端測試又叫功能測試,站在使用者視角,使用各種功能、各種互動,是使用者的真實使用場景的模擬。在產品高速迭代的現在,有個自動化測試,是重構、迭代的重要保障。對web前端來說,主要的測試就是,表單、動畫、頁面跳轉、dom渲染、Ajax等是否按照期望。

E2E測試驅動重構

重構程式碼的目的是什麼?是為了使程式碼質量更高、效能更好、可讀性和擴充性更強。在重構時如何保證修改後正常功能不受影響?E2E測試正是保證功能的最高層測試,不關注程式碼實現細節,專注於程式碼能否實現對應的功能,相比於單元測試、整合測試更靈活,你可以徹底改變編碼的語法、架構甚至程式設計正規化而不用重新寫測試用例。

Nightwatch

知道nightwatch是因為vue-cli工具安裝的時候會詢問是否需要安裝nightwatch。本身vue專案也是使用nightwatch來e2e測試的。nightwatch是一個使用selenium或者webdriver或者phantomjs的nodejs編寫的e2e自動測試框架,可以很方便的寫出測試用例來模仿使用者的操作來自動驗證功能的實現。selenium是一個強大瀏覽器測試平臺,支援firefox、chrome、edge等瀏覽器的模擬測試,其原理是開啟瀏覽器時,把自己的JavaScript檔案嵌入網頁中。然後selenium的網頁通過frame嵌入目標網頁。這樣,就可以使用selenium的JavaScript物件來控制目標網頁。

Nightwatch安裝

通過npm安裝nightwatch。

$ npm install [-g] nightwatch
複製程式碼

根據需要安裝Selenium-server或者其他Webdriver,比手動去下載jar檔案要方便很多。安裝哪些Webdriver取決於你想要測試哪些瀏覽器,如果只測試Chrome甚至可以不裝Selenium-server

$ npm install selenium-server
$ npm install chromedriver
複製程式碼

Nightwatch的配置

nightwatch的使用很簡單,一個nightwatch.json或者nightwatch.config.js(後者優先順序高)配置檔案,使用runner會自動找同級的這兩個檔案來獲取配置資訊。也可以手動使用--config來制定配置檔案的相對路徑。

{
  "src_folders" : ["tests"],
  "output_folder" : "reports",
  "custom_commands_path" : "",
  "custom_assertions_path" : "",
  "page_objects_path" : "",
  "globals_path" : "",

  "selenium" : {
    "start_process" : false,
    "server_path" : "",
    "log_path" : "",
    "port" : 4444,
    "cli_args" : {
      "webdriver.chrome.driver" : "",
      "webdriver.gecko.driver" : "",
      "webdriver.edge.driver" : ""
    }
  },

  "test_settings" : {
    "default" : {
      "launch_url" : "http://localhost",
      "selenium_port"  : 4444,
      "selenium_host"  : "localhost",
      "silent": true,
      "screenshots" : {
        "enabled" : false,
        "path" : ""
      },
      "desiredCapabilities": {
        "browserName": "firefox",
        "marionette": true
      }
    },

    "chrome" : {
      "desiredCapabilities": {
        "browserName": "chrome"
      }
    },

    "edge" : {
      "desiredCapabilities": {
        "browserName": "MicrosoftEdge"
      }
    }
  }
}
複製程式碼

json配置檔案大概就是上面這樣,分為基本配置、selenium配置和測試配置三個部分。基本配置依次為測試用例原始檔路徑、輸出路徑、基礎指令路徑、全域性配置路徑等。selenium設定包括是否開啟、路徑、埠等,cli_args指定將要執行的webdriver。test_settings制定測試時各個環境的設定,預設是default,通過--env加環境名可以指定配置的任意環境。只要把測試用例放在對應的資料夾使用module.exports暴露一個物件,其中key是測試名,value是一個接受browser例項的函式,在函式中進行斷言,nightwatch會自動依次呼叫資料夾中的測試用例。一個簡易的Chrome headless模式的nightwatch.conf.js配置如下:

{
    'src_folders': ['test/e2e/specs'],
    'output_folder': 'test/e2e/reports',
    'globals_path': 'test/e2e/global.js',
    'selenium': {
        'start_process': true,
        'server_path': require('selenium-server').path,
        'port': port,
        'cli_args': {
            'webdriver.chrome.driver': require('chromedriver').path
        }
    },

    'test_settings': {
        'default': {
            'selenium_port': port,
            'selenium_host': 'localhost',
            'silent': true,
            'globals': {
                'productListUrl': 'http://localhost:' + 9003 + '/productlist.html',
            }
        },

        'chrome': {
            'desiredCapabilities': {
                'browserName': 'chrome',
                'javascriptEnabled': true,
                'acceptSslCerts': true,
                'chromeOptions': {
                    'args': [
                       '--headless',
                     '--disable-gpu'
                    ],
	                'binary': '/opt/google/chrome/google-chrome'
                }
            }
        },

        'globals': {
            'productListUrl': 'http://localhost:' + 9003 + '/productlist.html',
        }
    }
}
複製程式碼

API

Nightwatch的API分為四個部分

1.Expect

在browser例項上以.expect.element開頭的BDD(行為驅動測試)風格的介面,0.7及以上版本nightwatch可用。通過.element方法傳入一個selector(參考querySelector或者jq的語法)獲取到dom例項,通過.text、.value、.attribute等方法獲取到例項屬性。還有一些語意明確的修飾:

  • to
  • be
  • been
  • is
  • that
  • which
  • and
  • has
  • with
  • at
  • does
  • of 再加上比較判斷:
.equal(value)/.contain(value)/.match(regex)

.selected

.present
複製程式碼

還有時間修飾.before(ms)(表示一段時間之內)、.after(ms)(表示一段時間之後)。就像造句一樣:某某元素的某某屬性(在某某時間)(不)等於什麼值,這就是BDD風格的測試程式碼。例如:

this.demoTest = function (browser) {
      browser.expect.element('body').to.have.attribute('data-attr');
      browser.expect.element('body').to.not.have.attribute('data-attr');
      browser.expect.element('body').to.not.have.attribute('data-attr', 'Testing if body does not have data-attr');
      browser.expect.element('body').to.have.attribute('data-attr').before(100);
      browser.expect.element('body').to.have.attribute('data-attr')
    .equals('some attribute');
      browser.expect.element('body').to.have.attribute('data-attr')
    .not.equals('other attribute');
      browser.expect.element('body').to.have.attribute('data-attr')
    .which.contains('something');
      browser.expect.element('body').to.have.attribute('data-attr')
    .which.matches(/^something\ else/);
};
複製程式碼

2.Assert

以.assert/.verify開頭的兩套相同的方法庫,區別是assert如果斷言失敗則退出整個測試用例所有步,verify則列印後繼續進行。

this.demoTest = function (browser) {
      browser.verify.title("Nightwatch.js");
      browser.assert.title("Nightwatch.js");
};
複製程式碼

有如下判斷方法:

.attributeContains(selector, attribute, expected[, message])
檢查指定元素(selector)的指定屬性(attribute)是否包含有期待的值(expected)列印出指定資訊(可選填的message)其他方法講解類似,不一一贅述

.attributeEquals(selector, attribute, expected[, message])
檢查元素指定屬性是否等於預期

.containText(selector, expectedText[, message])
包含有指定的文字

.cssClassPresent(selector, className[, message])
檢查元素指定class是否存在

.cssClassNotPresent(selector, className[, message])
檢查元素指定class是否不存在

.cssProperty(selector, cssProperty, expected[, message])
檢查元素指定css屬性的值是否等於預期

.elementPresent(selector[, message)
檢查指定元素是否存在於DOM中

.elementNotPresent(selector[, message)
檢查指定元素是否不存在於DOM中

.hidden(selector[, message)
檢查指定元素是否不可見

.title(expected[, message])
檢查頁面標題是否等於預期

.urlContains(expectedText[, message])
檢查當前URL是否包含預期的值

.urlEquals(expected[, message])
檢查當前URL是否等於預期的值

.value(selector, expectedText[, message])
檢查指定元素的value是否等於預期

.valueContains(selector, expectedText[, message])
檢查指定元素的value是否包含預期的值

.visible(selector[, message)
檢查指定元素是否可見
複製程式碼

3.Commands

很多命令的讀寫,可以操作BOM、DOM物件:

.clearValue(selector[, message])
清空input、textarea的值

.click(selector[, callback])
callback為執行完命令後需要執行的回撥

.closeWindow([callback])

.deleteCookie(cookieName[, callback])

.deleteCookies([callback])

.end([callback])
結束會話(關閉視窗)

.getAttribute(selector, attribute, callback)

.getCookie(cookieName, callback)

.getCookies(callback)

.getCssProperty(selector, cssProperty, callback)

.getElementSize(selector, callback)

.getLocation(selector, callback)

.getLocationInView(selector, callback)

.getLog(typeString, callback)
獲取selenium的log,其中type為string或者function

.getLogTypes(callback)

.getTagName(selector, callback)

.getText(selector, callback)

.getTitle(callback)

.getValue(selector, callback)

.init([url])
url方法的別名,如果不傳url則跳轉到配置中的launch_url

.injectScript(scriptUrl[, id, callback])
注入script

.isLogAvailable(typeString, callback)
typeString為string或者function,用來測試logtype是否可用

.isVisible(selector, callback)

.maximizeWindow([callback])
最大化當前視窗

.moveToElement(selector, xoffset, yoffset[, callback])
移動滑鼠到相對於指定元素的指定位置

.pause(ms[, callback])
暫停指定的時間,如果沒有時間,則無限暫停

.perform(callback)
一個簡單的命令,允許在回撥中訪問api

.resizeWindow(width, height[, callback])
調整視窗的尺寸

.saveScreenshot(fileName, callback)

.setCookie(cookie[, callback])

.setValue(selector, inputValue[, callback])

.setWindowPosition(offsetX, offsetY[, callback])

.submitForm(selector[, callback])

.switchWindow(handleOrName[, callback])

.urlHash(hash)

.useCss()
設定當前選擇器模式為CSS

.useXpath()
設定當前選擇器模式為Xpath

.waitForElementNotPresent(selector, time[, abortOnFailure, callback, message])
指定元素指定時間內是否不存在

.waitForElementNotVisible(selector, time[, abortOnFailure, callback, message])
指定元素指定時間內是否不可見

.waitForElementPresent(selector, time[, abortOnFailure, callback, message])

.waitForElementVisible(selector, time[, abortOnFailure, callback, message])
複製程式碼

簡單的例子:

this.demoTest = function (browser) {
    browser.click("#main ul li a.first", function(response) {
    this.assert.ok(browser === this, "Check if the context is right.");
    this.assert.ok(typeof response == "object", "We got a response object.");
    });
};
複製程式碼

4.webdriver protocol

可以操作一些更底層的東西,比如:

  • Sessions
  • Navigation
  • Command Contexts
  • Elements
  • Element State
  • Element Interaction
  • Element Location
  • Document Handling
  • Cookies
  • User Actions
  • User Prompts
  • Screen Capture
  • Mobile Related

簡單的例子:

module.exports = {
 'demo Test' : function(browser) {
    browser.element('css selector', 'body', function(res) {
      console.log(res)
    });
  }
};
複製程式碼

擴充

也可以單獨使用chromedriver等進行單一平臺測試,效率更高,測試更快。只需要npm安裝chromedriver或者其他webdriver,不需要selenium,在selenium設定中把selenium程式設定為false,測試環境配置中做出相應的改變。在golobal_path設定的配置檔案中,利用nightwatch測試的全域性before和after鉤子中開、關伺服器就好:

var chromedriver = require('chromedriver');

function startChromeDriver() {
  chromedriver.start();
}

function stopChromeDriver() {
  chromedriver.stop();
}

module.exports = {
  before : function(done) {
    startChromeDriver.call(this);
    done();
  },

  after : function(done) {
    stopChromeDriver.call(this);
    done();
  }
};
複製程式碼

配置尤雨溪大神的nightwatch-helpers食用更佳,補了一些api。Assertions:

  • count(selector, count)
  • attributePresent(selector, attr)
  • evaluate(fn, [args], [message])
  • checked(selector, expected)
  • focused(selector, expected)
  • hasHTML(selector, html)
  • notVisible(selector)

Commands:

  • dblClick(selector)
  • waitFor(duration)
  • trigger(selector, event[, keyCode])
  • enterValue(selector, value)

只需要在圖中位置配置一下即可

image.png

其他

推薦使用Headless測試即不開啟瀏覽器可視介面以便能跑在伺服器上。比如Phantomjs可以模擬webkit核心瀏覽器的行為,在Nightwatch中配置一下Phantomjs環境即可,啟動nightwatch時使用--env加上配置裡的環境名啟用對應的環境。如今(59版本以上)Phantomjs已經停止維護,使用Chrome自帶的headless模式是更好的選擇。也可以使用Puppeteer來做E2E測試,好處是隻依賴一個Puppeteer,並且API相對簡單。

相關文章