使用nightwatch进行E2E测试中文教程

7,526 阅读7分钟

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相对简单。