使用 Protractor作

AngularJS End to End 測試

 

Idea behind E2E 

  • 使用者觀點
  • 後端與前端
  • 增加釋出程式碼的信心度
  • 不可以替代單元或整合測試

End to End

  • Web-level 測試
  • 必須有特定的server
  • Selenium / Protractor

Unit Test

  • 片斷程式碼測試
  • 測試services, classes and objects
  • Karma

Selenium vs Protractor ?

   Protractor 輸出全域element(), 包含一個Locator參數, 回傳ElementFinder物件,然後selenium會去browser裡面找到該element。

安裝

  1. Download Node.JS

npm install protractor -g
webdriver-manager start
webdriver-manager update

Step0 -

準備兩個檔案

  • conf.js

  • spec.js 

設定 conf.js

//conf.js
exports.config = {
  framework: 'jasmine2',
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['spec1.js']
}

開始撰寫測試檔案

// spec.js
//Jasmine 語法 - describe: 描述這個測試
describe('Protractor Demo App', function() {
  //Jasmine語法 - it: 這個測試會做什麼?
  it('should have a title', function() {
    //Protractor 提供的global變數與方法, 告訴selenium去載入頁面
        browser.get('http://localhost:1232/');
    
    //This is wrong
    expect(browser.getTitle()).toEqual('易发国际a'); 
    });
});

Step 1 -
與elements(locator)互動

Element(Locator)

// 使用css選擇器
element( by.css('.myclass') ); 

//  使用id
element( by.id('myid') ); 

// 使用 ng-model
element( by.model('name') ); 

// 綁定某變數的元素
element( by.binding('bindingname') ); 

Element & Locator

// spec2
describe('Protractor Demo App', function() {
  it('should add one and two', function() {
    browser.get('http://juliemr.github.io/protractor-demo/');
    element(by.model('first')).sendKeys(1);
    element(by.model('second')).sendKeys(2);
    element(by.id('gobutton')).click();

    expect(element(by.binding('latest')).getText()).
        toEqual('5'); // This is wrong!
  });
});

後面加上Action

 var el = element(locator);

        // Click on the element
        el.click();

        // Send keys to the element (usually an input)
        el.sendKeys('my text');

        // Clear the text in an element (usually an input)
        el.clear();

        // Get the value of an attribute, for example, get the value of an input
        el.getAttribute('value');

所有的方法都是非同步
且回傳Promise

// Example of getText() promise
element( by.model('zipcode') ).getText()
  .then(function(val) {
    var num = val.substring(0, 4);
    var isNum = !isNaN(num);
    expect( isNum ).toBeTruthy();
  });

Control Flow

it('should find an element by text input model', function() {
  browser.get('#/home'); // (1) method browser.get

  // (2) method by.binding
  var login = element(by.binding('login'));
  // (3) method getText
  expect(login.getText()).toEqual('User');
});

WebDriverJS 會控制Control Flow

測試lobby/Live中
路徑是否正確

// test-live.js
describe('Protractor Demo App', function() {
  //在每個it 共用的code
  afterEach(function() {
    browser.igNoreSynchronization = false;
});
  //測Xtd
  it('should test XTD link', function() {
    browser.get('http://localhost:1232/Lobby/Live');
    browser.ignoreSynchronization = true;
    element(by.css('[data-box="01"]')).click().then(function () {
        browser.getAllWindowHandles().then(function (handles) {
            browser.switchTo().window(handles[1]).then(function () {
                expect(browser.getCurrentUrl()).toEqual('http://localhost:1232/Account/LoginToXTD');
                browser.driver.switchTo().window(handles[0]);
            });
        })
    });
}); 

//BB
it('should test BB link is valid', function() {
    browser.get('http://localhost:1232/Lobby/Live');
    browser.ignoreSynchronization = true;
    element(by.css('[data-box="02"]')).click().then(function () {
        browser.getAllWindowHandles().then(function (handles) {
            browser.switchTo().window(handles[1]).then(function () {
                expect(browser.getCurrentUrl()).toEqual('http://777.gpk-online.com/');
                browser.driver.switchTo().window(handles[0]);

            });
        })
    });
}); 
browser.igNoreSynchronization = false;
//AG
it('should test AG link is valid', function() {
    browser.get('http://localhost:1232/Lobby/Live');
    browser.ignoreSynchronization = true;
    element(by.css('[data-box="03"]')).click().then(function () {
        browser.getAllWindowHandles().then(function (handles) {
            browser.switchTo().window(handles[1]).then(function () {
                expect(browser.getCurrentUrl()).toEqual('http://localhost:1232/Account/LoginToAg?lunchGame=0');
                browser.driver.switchTo().window(handles[0]);
            });
        })
    });
}); 
browser.igNoreSynchronization = false;
  //GPK
  it('should test GPK link is valid', function() {
    browser.get('http://localhost:1232/Lobby/Live');
    browser.ignoreSynchronization = true;
    element(by.css('[data-box="04"]')).click().then(function () {
        browser.getAllWindowHandles().then(function (handles) {
            browser.switchTo().window(handles[1]).then(function () {
                expect(browser.getCurrentUrl()).toEqual('http://gpk-gameinfo.com/');
                browser.driver.switchTo().window(handles[0]);
            });
        })
    });
}); 

  browser.igNoreSynchronization = false;
  //MG
  it('should test MG link is valid', function() {
    browser.get('http://localhost:1232/Lobby/Live');
    browser.ignoreSynchronization = true;
    element(by.css('[data-box="05"]')).click().then(function () {
        browser.getAllWindowHandles().then(function (handles) {
            browser.switchTo().window(handles[1]).then(function () {
                expect(browser.getCurrentUrl()).toEqual('http://localhost:1232/Account/LoginToMgLiveDealer?startTab=0');
                browser.driver.switchTo().window(handles[0]);
            });
        })
    });
}); 
  browser.igNoreSynchronization = false;
});


//refernce: http://stackoverflow.com/questions/28511013/non-angular-page-opened-after-a-click

可維護的測試

Best practices 

Page Objects

Exports; Require

Test specs

Page Object

  1. Duplication
  2. Coupling
  3. ​Maintenance

Design Pattern

interface

 If UI changes, only PO changes

Page Object

//test-live.po.js
'use strict';

  var LivePage = function() {
    this.xtd = element(by.css('[data-box="01"]'));
    this.bb = element(by.css('[data-box="02"]'));
    this.ag = element(by.css('[data-box="03"]'));
    this.gpk = element(by.css('[data-box="04"]'));
    this.mg = element(by.css('[data-box="05"]'));

    this.get = function(){
        browser.get('http://localhost:1232/Lobby/Live');
    }
  };
  module.exports = new LivePage();

Spec

//test-live2.js

describe('Protractor Live test', function() {

  var testLive = require('./test-live.po.js');
  afterEach(function() {
    browser.igNoreSynchronization = false;
  });

  //測XTD
  it('should test XTD link', function() {
    testLive.get();
    browser.ignoreSynchronization = true;
    testLive.xtd.click().then(function () {
        browser.getAllWindowHandles().then(function (handles) {
            browser.switchTo().window(handles[1]).then(function () {                                     expect(browser.getCurrentUrl()).toEqual('http://localhost:1232/Account/LoginToXTD');
                browser.driver.switchTo().window(handles[0]);
            });
        })
    });
  }); 
});

測試不同瀏覽器 - conf2.js


exports.config = {
  framework: 'jasmine2',
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['test-live.js'],
  multiCapabilities: [{
    browserName:'firefox'
  },{
    browserName:'chrome'
  }]
}

結語

  • E2E測試不可取代單元測試

  • 注意擴充性

  • 其他工具

elementexplorer

gulp-protractor-qa

測試註冊頁面

Protractor

By Eric Wang

Protractor

An Introduction to AngularJS End to End Testing using Protractor

  • 1,086