Protractor – 實戰篇 – mobile web & web service support

Protractor 是一款 end-to-end test framework. 什麼是 end-to-end test 呢?! 簡單的說它可以模擬 user 在 browser 上操作的所有行為(近乎 100%), 進而測試頁面 flow 是否順暢? JavaScript 各 module 是否運作正常? 如果寫得好的話, 取代傳統的人工測試使之進化到全自動化測試也不再是件遙不可及的夢想。

筆者撰寫本文的時候, 尚在 Yahoo 奇摩 – 拍賣 擔任 front end engineer 的職務, 2016 年 Q1, 拍賣有個全力衝刺的新功能誕生 – 「 議價 」, 只要賣家商品有開放議價功能的話, 那麼買家便可以隨時隨地跟賣家發動議價, 藉此取得兩者皆可以接受的最合理價格, 不僅買家可以開開心心用自己心儀的價錢購得喜歡的商品, 賣家也可以因此迅速的自己的商品進行出清, 該功能推出後, 比起一般的商品而言, 有開啟議價功能的商品之訂單成立率確實高出許多喔! 這也是拍賣近年來難得深受買 、 賣家雙方親賴歡喜的新功能

除此之外, 「 議價 」它還有一個拍賣從沒有過的創舉, 那就是…它是一個 APP 獨享的功能, 也就是說 desktop 上僅有開啟商品議價的功能而已, 想要發動或者接受議價的話, 只能透過 Yahoo 奇摩 – 拍賣的 Android / iOS APP 才能做喔!

以下為相關的 flow:

新增/編輯 商品時, 可以選擇是否開起議價功能, 不管是 APP 還是 desktop 都有支援

對商品有興趣的買賣家透過類 IM 的操作來取得一個雙方都可以接受的價位 (APP only)

買賣家皆有相關列表頁可以快速進行議價的行為 (APP only)

完成議價的商品直接透過 app 結帳流程來進行購買行為 (APP 內嵌 mobile web)

以上便是 Yahoo 奇摩 – 拍賣的議價功能, 想要更進一步了解的話, 亦可點擊相關連結 – APP 限定議價功能 進行一覽

隨著 APP 的抬頭, 現在有越來越多的功能都是先行在 APP 上進行實裝, 一方面求新求快, 另一方面也可以當作新功能是否在現行市場上存活的依據之一, 一旦深受 user 的喜愛, 未來再慢慢的將之移植回 desktop 也不無不可

mobile 興起之初, 很多服務都是先有 desktop, 所以那時的開發模式主要以 desktop version mobilize (APP / mobile web), 現在則恰好相反, 以 mobile 為主, 確立值得繼續投資後, 才會慢慢的將之移值回 desktop 上, 對於開發模式的轉換, 個人實感興奮卻又不免有些感傷與不適 那麼…既然是 APP 限定的功能, 那麼 Protractor 能夠使上什麼力呢?!

想要幫這樣子的功能進行把關, 偏偏卻又尋不著其使力點!

就在一度想要放棄的當下, 似乎看到了那麼一線曙光啊?! 原來「議價」相關功能已經建立好一套完整的 Web Service, APP 相關的功能操作實際上都是 based on 這套 API 之上, 也就是說…即使現行的 desktop 並不支援這樣的一個 feature, 但是, 我們可以透過 call 相關的 API 便可以模擬出以下行為

  • 買家發動議價
  • 賣家接受議價

以上兩個行為即表一個議價事件的成立, 只要把這樣的行為封裝到相關的 pageObject 中, 未來如果 desktop 有支援相關的功能後, 只要修改相關的 pageObject – method 的調整, 便可以合乎時宜且為未來帶來極大的彈性

另外, 由於 APP 得結帳流程實際上是嵌入 webView 相關的實作, 簡單的說就是與 mobile web 進行某種程度的 hybrid, 也就是說…只要有辦法 handle 下列這兩件事情, 那麼…我便可以完整的幫這個新功能進行 Protractor end to end testing 了

  1. 測試載具模擬 mobile web 的 user agent
  2. 有效的 call 相關的 Web Service, 來進行某些行為的發動

接下來便是要跟大家介紹筆者是如何透過 Protractor 來達到以上的需求

※ 模擬 mobile web 的呈現

Protractor conf 針對 browser 的設定除了可以設定當前想要進行測試的瀏覽器外, 其實也可以帶入其他參數來模擬當前的 user agent, 透過 user agent 的設定, 我們便可以很容易的將當前測試載具模擬成 mobile 的狀態

Protractor 官方 config setting: git

只要設定 capabilities.chromeOptions 即可調整當前的 user agent, 以下為模擬 Android Nexus 6 device … (git)

capabilities.chromeOptions = {    args: [        '--user-agent="Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.20 Mobile Safari/537.36"'    ]};

為了讓它能更容易的使用, 所以筆者將它抽出成環境變數, 透過環境變數的調整, 我們便可以很快的決定當前的測試使否要以 mobile web 的形式來進行測試, 以下為結合 suiteQueue.js 所做的設定

node suiteQueue.js dragonBargain+device=mobile

透過 device=mobile 的設定, 便可以快速的進行切換

※ call Web Service

接下來便是透過 web service 來進行一些狀態的達成, 這類的應用通常蠻廣範的, 雖然說可以透過 page 直接發出 XHR (XmlHttpRequest) 來做, 不過一但遇到一些需要進行跨網域且無 CROS 設定的 web service 時, 便無用武之地了, 所以請儘量以 service 來答成這類的需求!

議價相關的 web service 取得 token 的方式比較特別, 它需要使用 user login 的 cookie 去換取相關的 token, 拿到 token 之後, 才能進行一些行為的操作, 筆方說買家發起議價, 賣家接受議價…etc.

相這類取得 token 的方式有可能在各個 page 發生, 因此, 筆者再開發這段 code 的時候, 決定將它封裝在 pageObject 這個 super class 裏面, 讓每個 page 的 instance 都可以繼承它

pageObject: git

接下來我們看看 pageObject 取得 authToken 的實作部分, 相關的流程如下

  1. 取得當前 login user 的 cookie
  2. 透過 common.parse_url 來解析 uri, 決定一些基礎設定, 比方說 require 的 lib (http || https), 並且將相關變數設定好
  3. 將 step.1 取得的 cookie, 透過 join(‘;’) 這個 method 將他組成 string, 並且塞入 http config 的 headers.Cookie 來將 cookie 往 web service 做傳遞
  4. http connection 發動時, 利用 promise 來確保 response 的順序性
  5. 回傳相關取得到的 token
getAuthToken: function() {    var uri = constants.AUTHTOKEN_URL, data;    data = {        authToken: '',        cookie: []    };        return browser.manage().getCookies().then(        function(c) {            c.forEach(                function(unit, idx) {                    return data.cookie.push(unit.name+'='+unit.value);                }            );        }    ).then(        function() {            var cfg, deferred, uri, http;            uri = common.parse_url(constants.AUTHTOKEN_URL);                        http = require(uri.scheme);            cfg = {                headers: {                    'Cookie': data.cookie.join(';'),                    'Content-Type': 'application/json'                }            };            cfg.host = uri.host;            cfg.path = uri.path;            if (uri.query) cfg.path += '?' + uri.query;            if (uri.port) cfg.port = uri.port;
deferred = protractor.promise.defer();
http.get(cfg, function(response) { var resBody = ''; response.on('data', function(d) { resBody += d; }); response.on('end', function() { var resTxt = JSON.parse(resBody); data.authToken = resTxt.query.results.authToken; deferred.fulfill(true); }); });
return deferred.promise; } ).then( function() { return data; } );}

… (git)

以上便是取得 authToken 的相關 code, 筆者透過 common.parse_url 來進行 uri 的解析, 這個 method 便是跟 PHP 的 parse_url 的 response 一樣, 同樣的 format 可以讓我們快速的進行經驗移轉, 不需要增加額外的學習成本 (筆者較為熟悉 PHP)

common.js … (git)

接下來筆者假設日後 desktop 的議價發動與接受都會在 itemPage 裡進行, 於是我們先把相關的 method 封裝在 itemPage 的 pageObject 中, 日後一旦 desktop release 相關議價的功能且操作有變動時, 我們便可以很快的知曉要去哪裡更改了!!

  • features … (git)
@ BARGAINONScenario: [商品頁]buyer can on a bargain event    Given I login as "buyer_general"    And I visit "itemPage - buyNow - basic"    Then I bargain as price "5" must correct
@ BARGAINACCETPScenario: [商品頁]seller can accept a bargain event Given I login as "seller_store_b2c" And I visit "itemPage - buyNow - basic" Then I accept a specific bargain must correct
  • stepDefinition … (git)
Then(/^I bargain as price "([^"]*)" must correct$/, function(key, next) {    var world = this;    //buyer on a bargain event    browser.params.bargain = {        price: key    };
//itemPage this.stand.originateBargain(key).then( function(request) { expect(request, 'originateBargain function fail').not.to.be.undefined; world.bargainId = request; browser.params.bargain.id = request; } );});
Then(/^I accept a specific bargain must correct$/, function(next) { //itemPage this.stand.acceptBargain(browser.params.bargain).then( function(flag) { expect(flag, 'acceptBargain function fail').to.be.true; } );});

透過 stepDefinition 的定義, 我們可以指定 feature 遇到哪些句子需要做哪些處理, 我們可以清楚的看到發動議價之後, 我們會把相關的資料存放在

browser.params.bargain = {    id: "1234567890",    price: "5"};

最後賣家接受議價時的驗證, 便是將這些資料丟進去比對, 便可以知道是否接受議價成功.

從上面的 stepDefinition 我們可以看到 originateBargain 以及 acceptBargain 這兩個 method 主要是被封裝在 itemPage 這個 pageObject instance 中

  • originateBargain … (git)
  • acceptBargain … (git)

這兩個 method 發動前都要先取得 authToken 才能做後續的操作, 取得 authToken 後, 其 call web service 的方法就跟 getAuthToken 一樣, 也是 require 相關的 lib 進來, 唯一比較不同的部分, 便是由於它一些資料的傳遞, 所以在寫法上略為不同, 需要使用 write 以及 end 的 method 來做

function() {    var cfg, deferred, http, postData, req, uri;
uri = common.parse_url(constants.BARGAIN_ON); http = require(uri.scheme); postData = { listings: [ { id: stand.data.itemId, quantity: '1' } ] message: '我要議價', price: key };
postData = JSON.stringify(postData); cfg = { method: 'POST', headers: { 'Cookie': data.cookie.join(';'), 'Content-Type': 'application/json', 'X-YahooWSSID-Authorization': data.authToken } }; cfg.host = uri.host; cfg.path = uri.path; if (uri.query) cfg.path += '?' + uri.query; if (uri.port) cfg.port = uri.port; deferred = protractor.promise.defer();
req = http.request(cfg, function(response) { var resBody = ''; response.on('data', function(d) { resBody += d; }); response.on('end', function() { bargainData = JSON.parse(resBody); deferred.fulfill(true); }); }); req.write(postData); req.end();
return deferred.promise;}

透過這樣的設置之後, 我們便可以透過 Protractor 來 call web service 來達到一些特定的需求, 只要有類似的需求, 均可以透過這樣的寫法來做一些微調, 讓整體測試的觸手可以更加的寬廣以及深入

dragonBargain testing Video

兩個主要的癥結點克服之後, 我們便可以開開心心的來為「議價」進行測試把關, 以下為相關的測試範圍

  1. 新增一筆無規格直購商品
  2. 商品管理頁議價功能 on / off 是否正常
  3. 前往 itemPage 驗證 bargain mark 是否出現
  4. 切換 buyer 身份, 發動議價是否成功
  5. 切換 seller 身份, 接受議價是否成功
  6. 能否將議價待結帳經由 郵局貨到付款 成立訂單 (mobile web)
  7. 賣家執行出貨是否成功
  8. 買家給賣家評價是否成功
  9. 賣家給買家評價是否成功
  10. 將上述商品進行下架

youtube vide: https://www.youtube.com/watch?v=0-sZZbtvGc4

youtube vide: https://www.youtube.com/watch?v=0-s…

歡迎有興趣的朋友點擊相關連結進行一覽

※ Reference :

※ 延伸閱讀 :

轉載文章 – Protractor – 實戰篇 – mobile web & web service support

mei