淺談網站自動化測試

以 WebdriverIO 框架為例

劉艾霖 (Alin)

遠距工作者

alincode

NodeJs developer ( 1 year)

SDET ( 8 month)

Java developer ( 5 years )

RD ---> SDET

2014/04 ~ 2014/12

 1. 撰寫自動化測試程式

2. 建立測試架構

Jenkins - CI / CD

CI / CD

email test report

SpiraTest

真的這麼順利嗎?

write jenkins plugin

RD ---> SDET --> RD

曾經擔任過 SDET 帶給我的轉變

測試的相關資料不多

Official documents

Test Case

Youtube video

是你最好的好朋友

but now...

Summery

網站自動化測試

Webdriver.IO 介紹

Getting Started

工具介紹

建構測試環境

快速建立前端測試 & Live demo

關於導入的建議

Summary

網站自動化測試

  • 透過自動化的方式,去控制流程

  • 比較實際結果預期結果之間的差異

  • 反覆執行

 導致的問題是?

前端測試框架

挑選準則

資源:功能、工具、文件

擴充:架構容不容易擴充

永續性:PR 多不多

開發者面向:容不容易debug

nightwatch

protractor

webdriver.io

PR

webdriver.io

WebdriverIO

!=

WebdriverJS

Webdriver

架構

selenium-server restful api

高度模組化

Extendable

custom reporter

var util = require('util'),
    events = require('events');

var CustomReporter = function(options) {

};

// Inherit from EventEmitter
util.inherits(CustomReporter, events.EventEmitter);

// Expose Custom Reporter
exports = module.exports = CustomReporter;
// wdio.config
var CustomReporter = require('./reporter/my.custom.reporter');
exports.config = {
    reporters: [CustomReporter],
};

custom commands

browser.addCommand('doExternalJob', function async (params) {
    return externalLib.command(params);
});
it('execute external library in a sync way', function() {
    browser.url('...');
    browser.doExternalJob('someParam');
    console.log(browser.getTitle()); 
});

Plugin

webdrivercss

Getting Started

框架幫我們做了哪些事

簡化 API

client
    .url('http://google.com')
    .setValue('#q', 'webdriver')
    .click('#btnG')

// Chain Selectors

WebdriverJS

driver.get('http://www.google.com');
driver.findElement(webdriver.By.id('q')).sendKeys('webdriver');
driver.findElement(webdriver.By.id('btnG')).click();

WebdriverIO

lib/commands/click.js

import { RuntimeError } from '../utils/ErrorHandler'

let click = function (selector) {
    return this.element(selector).then((elem) => {
        // lib/helpers/findElementStrategy.js
        if (!elem.value) throw new RuntimeError(7);

        return this.elementIdClick(elem.value.ELEMENT)
    })
}

export default click

selectors

  • CSS Query Selector (建議使用)
  • Link Text
  • Partial Link Text
  • Element with certain text
  • Tag Name
  • Name Attribute
  • xPath (不建議使用)
browser.click('h2.subheading a');

API 介紹

撰寫測試的三步驟

1. 操作行為
2. 取得值
3. 判斷結果

從類別來看

  • Action

  • Property

  • Protocol

  • State

  • Utility

  • Mobile (略)

Action

// click
browser.click('#myButton');

// setValue
var input = browser.element('.input');
input.setValue('test123');

// submitForm
browser.submitForm('#loginForm');

Property

// title
browser.getTitle();

// get value
browser.getValue('.input');

// get url
browser.getUrl();

Protocol

// single element
browser.element('.h2');

// Collection of elements
browser.elements('li');

// url
browser.url('http://webdriver.io');

State

browser.isEnabled(selector);

browser.isExisting(selector);

browser.isSelected(selector);

Utility

browser.element('.notification').waitForExist(5000);

browser.pause(5000);

browser.debug();

browser.saveScreenshot('front_page.png');

Utility

// ends session and close browser
client
    .init()
    .url('http://google.com')
    .end();


browser.addCommand();

管理 test case

Mode

  • Standalone Mode

  • The WDIO Testrunner

WDIO

 a test runner that helps you to build a reliable test suite that is easy to read and maintain.

BDD / TDD framework

var assert = require('assert');

describe('blog front page', function() {
  it('should have the right title', function() {
    browser.url('http://alincode.github.io/blog/');
    var title = browser.getTitle();
    assert.equal(title, 'alincode');
  });
});

with Mocha

var webdriverio = require('webdriverio');
var options = { desiredCapabilities: { browserName: 'chrome' } };
var client = webdriverio.remote(options);
client
    .init()
    .url('https://duckduckgo.com/')
    .setValue('#search_form_input_homepage', 'WebdriverIO')
    .click('#search_button_homepage')
    .getTitle().then(function(title) {
        console.log('Title is: ' + title);
        // outputs: "Title is: WebdriverIO (Software) at DuckDuckGo"
    })
    .end();

Standalone Mode

Test Specs Group

// wdio.conf.js
exports.config = {

    // define all tests
    specs: ['./test/specs/**/*.spec.js'],      

    // define specific suites
    suites: {
        login: [
            './test/specs/login.success.spec.js',
            './test/specs/login.failure.spec.js'
        ],
        otherFeature: []
    }
}
wdio wdio.conf.js --suite login
wdio wdio.conf.js --suite login,otherFeature

支援Promised

client
    .init()
    .url('https://webdriver.io/')
    .getTitle().then(function(title) {
        console.log(title);
        // outputs: "WebdriverIO - Selenium 2.0 javascript bindings for nodejs"
    })
    .end();

default promised

var client = require('webdriverio').remote({
    desiredCapabilities: {
        browserName: 'chrome'
    }
});
 
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
chai.Should();
chai.use(chaiAsPromised);
chaiAsPromised.transferPromiseness = client.transferPromiseness;

Transfer promises

describe('my app', function() {
    before(function () {
        return client.init();
    });
 
    it('should contain a certain text after clicking', function() {
        return client
            .click('button=Send')
            .isVisible('#status_message').should.eventually.be.true
            .getText('#status_message').should.eventually.be.equal('Message sent!');
    });
});

Transfer promises

browser object

  • webdriver instance

  • globel object

  • 取得設定檔內的自訂變數
browser.options.bio
// wdio.conf.js

exports.config = {
    // ...
    bio: 'hello world',
    // ...
}

自訂變數

async vs sync

sync: true

wdio.conf.js

synchronize

describe('DuckDuckGo search', function() {
    it('searches for WebdriverIO', function() {
        browser.url('https://duckduckgo.com/');
        browser.setValue('#search_form_input_homepage', 'WebdriverIO');
        browser.click('#search_button_homepage');
        var title = browser.getTitle();
        console.log('Title is: ' + title);
    });
});

asynchronous 可以搭配 await

Reporter

Dot

Spec

and more...

Teamcity

Allure

JSON

Page Object Pattern

function Page() {}

Page.prototype.open = function(path) {
  browser.url('/' + path)
}

module.exports = new Page();
// login.page.js
var Page = require('./page')

var LoginPage = Object.create(Page, {

    // define elements
    username: { get: function () { return browser.element('#username'); } },
    password: { get: function () { return browser.element('#password'); } },
    form:     { get: function () { return browser.element('#login'); } },
    flash:    { get: function () { return browser.element('#flash'); } },

    // define or overwrite page methods
    open: { value: function() {
        Page.open.call(this, 'login');
    } },

    submit: { value: function() {
        this.form.submitForm();
    } }
});

module.exports = LoginPage
// login.spec.js
var expect = require('chai').expect;
var LoginPage = require('../pageobjects/login.page');

describe('login form', function () {
    it('登入失敗', function () {
        LoginPage.open();
        LoginPage.username.setValue('foo');
        LoginPage.password.setValue('bar');
        LoginPage.submit();

        expect(LoginPage.flash.getText()).to.contain('Your username is invalid!');
    });

    it('登入成功', function () {
        LoginPage.open();
        LoginPage.username.setValue('tomsmith');
        LoginPage.password.setValue('SuperSecretPassword!');
        LoginPage.submit();

        expect(LoginPage.flash.getText()).to.contain('login success');
    });
});

除此之外

connectionRetryTimeout: 90000,
  connectionRetryCount: 3,

工具介紹

generate config

browser driver manager

連接 e2e 跟 selenium-server

java -jar selenium-server-standalone-2.42.2.jar

chimp

chimp --mocha --watch --path=test
const assert = require('assert');

describe('Google search', function() {
  it('case 1: @watch', function() {
    browser.url('http://www.google.com/ncr');
    browser.setValue('[name=q]', 'alincode blog');
    browser.click('[name=btnG]');
    assert.equal(browser.getTitle(), 'Google');
  });
});

@focus,@dev,@watch

execute

建構測試環境

cloud hub

local hub

cloud hub

Sauce Labs
Browserstack
TestingBot

build it by youself

build it by youself

  • CI server

  • seleinum hub

  • vm node and driver

but
Not recommended

Automated

但其實不要建Hub也可以

只是缺點是...

快速建立前端測試

  • 初始化專案

  • 建立設定檔

  • 撰寫測試程式

  • 執行

初始化專案

mkdir jsdc-webdriverio-sandbox
cd jsdc-webdriverio-sandbox
npm init -y
// install package

npm i webdriverio -D

建立設定檔

./node_modules/webdriverio/bin/wdio

多選,請按空白鍵

vi wdio.conf.js

capabilities: [{
  browserName: 'chrome'
}],
mochaOpts: {
  timeout: 60000
},

init folder

mkdir -p ./test/specs/
mkdir -p ./errorShots/

vi test/specs/test.js

var assert = require('assert');

describe('page', function() {
  it('blog', function() {
    browser.url('http://alincode.github.io/blog');
    var title = browser.getTitle();
    assert.equal(title, 'alincode');
  });
});

execute

./node_modules/webdriverio/bin/wdio wdio.conf.js

npm test

"scripts": {
  "test": "wdio wdio.conf.js"
}

Live Demo

關於導入的建議

Question:

網站自動化測試,在一個已經上線的網站是否有建議從什麼地方開始做起,新功能舊功能?

10 %

20 %

70 %

最重要的功能開始

Question:

但如果不補單元測試,直接開始寫前端測試會有什麼後果?

  • 不好維護的測試程式

  • 難以判斷測試結果

導入是一個組合性的問題

先請RD來建構整個架構

找曾經踩過雷的人

~~~緣分

Summary

使用 NodeJS 寫前端測試

很幸福的

full-time remote

backend engineer