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

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,用来测试log的type是否可用

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

欢迎来我博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容