概述
傳統的QA自動化測試通常是基於GUI的,比如使用Selenium,模擬用戶在界面上操作。但GUI測試的開發、維護成本和運行的穩定性一直是測試界的老大難問題。投入大量的人力物力開發、維護、運行,卻得不到相應的回報,令許多同行頭痛不已。不過端對端(end to end)測試確實是QA/測試團隊的重點工作之一,是繞不過的坎,怎么破?今天就分享一下基於API(HTTP層面)的自動化測試,姑且叫它“半端對端 (semi end to end)”吧。其實我認為它已經接近95%端對端了,為什么這樣說?
假設有一個測試用例:
第1步:輸入數據1
第2步:輸入數據2
第3步:輸入數據3
第4步:校驗前面輸入的數據
第5步:輸入數據5
...
第n步:保存
第n+1步:輸入查詢條件
第n+2步:查詢剛才保存的記錄
第n+3步:校驗查詢結果
GUI的自動化測試通常會完全模擬上面所有步驟,每一步都要識別相應要操作的界面元素,進行輸入點擊等操作,或者從界面上摳出數據,進行校驗。運行過程中任何一步的某個元素定位不到,或者任何原因的操作失敗都會導致整個測試中斷。
那么在API層面怎么做上面的測試呢?
第1步:發送上面第n步的“保存”操作的HTTP,得到response,並記錄里面返回的ID等有用信息。可以直接跳過前面所有步驟是因為,保存時所發送的HTTP請求里面已經包含了所有前面輸入的並且經過校驗的有效數據。
第2步:發送上面第n+2步的查詢操作的HTTP請求,得到response,並校驗里面的結果
對,就這么簡單。理論上跟GUI上的測試效果是接近的,除了一些純界面上的邏輯(這些通常並不是我們的回歸測試重點,起碼在我所經歷的項目中)。最大的好處是完全不碰界面,極大的消除了操作GUI所帶來的開發、維護成本和運行的不穩定性。也許你有很多疑問,沒關系,接着看完也許就有了解答。
實踐當中可以進行HTTP測試的庫選擇很多,大多數編程語言都有現成的HTTP庫可以使用,比如python、java等。這里使用現在流行的node.js進行講解。原因很簡單,大家知道HTTP是無狀態的,多個HTTP請求之間通常沒有互相依賴,大多數情況下沒有必要讓測試一個個的跑完一個再跑另一個,所以自然就想到讓多個HTTP請求並行測試,可以極大的提高效率減少測試時間。node.js的最強項就是非阻塞的異步I/O,是理想的測試HTTP的平台。這里使用了一個第三方的HTTP客戶端superagent,大家可以從npm下載,到它的github頁面查看API文檔。
原理
這種API測試的核心原理是,首先保存一個離線的期望結果,然后調用HTTP請求,把實時返回的response與期望結果進行對比,可進行文本對比,或者JSON對比,大多數REST服務返回的都是JSON格式數據。
聽起來似乎很多工具比如SoapUI也可以做到啊,為什么要自己開發呢,原因就是對於企業系統或者任何較大規模的系統來講需要批量測試成百上千甚至幾千個web service,不同的service需要靈活定制對結果的處理並校驗,生成、發送測試報告,定期運行,甚至與運維工具進行集成等等,第三方工具沒有這么大的靈活性。
從某種程度上講API測試也很像性能測試,不過我們比性能測試更加關心對返回結果的校驗。
下面是主要工作流程的詳細解釋:
錄制HTTP:
首先,上面提到的期望結果從哪里來?你可以自己動手寫出來保存到一個文件里面,但是對於回歸測試來講這樣太原始,在一些企業級應用里面HTTP不管request還是response數據都大的驚人(很多需要進行壓縮),數據結構也很復雜,幾乎不可能純手工進行定制。回歸測試通常需要進行大量API測試,一個個手寫期望結果也不科學。所以最好的辦法就是使用工具進行HTTP錄制。可以用的工具很多,但其中最強大的非Fiddler莫屬。Fiddler的設置這里不詳述,大家可以自行谷歌。
Fiddler設置好了以后(主要是把瀏覽器的代理指向它,並且打開Decode選項,如下圖),就可以在界面上把測試用例手動操作一遍,Fiddler就會完整無缺的把所有的HTTP的request和reponse錄制下來,當然可以設置一些過濾條件,把那些下載js, css腳本的,下載圖片的http濾掉,只保留純粹的與服務器進行數據交換的服務,這才是我們要測試的東西。把錄到的所有http的request和response保存為文本文件。

保存好的http文本是標准的http協議的格式包括header, body等部分。
解析HTTP文本:
測試過程的第一步是把所有錄制下來要測試的HTTP一個個解析出來放到javascript對象里面,以備下一步回放時候調用及校驗結果。這里是解析出來的樣子:

解析的過程使用了node.js的原生API 'readline',逐行讀取文本文件,把相應信息寫入Javascript對象:
{ req_method: '', req_endpoint: '', req_headers: {}, req_body: '', res_headers: {}, res_body: '' }
執行准備工作(set up):
首先取得登錄信息:
在正式測試之前通常需要先登錄你要測試的系統,取得登錄cookie,然后把這個cookie替換掉之前錄制的http頭部的cookie信息,才能順利回放所有的http。登錄的過程每個系統都不同,需要自己加以研究。我所測試的系統需要按順序調用5個web service才能最終拿到包含登錄信息的cookie。研究時,需要在瀏覽器里面進行實際的登錄操作,然后每一步都研究一下http request和reponse,基本上每一步的response(或者在header里面或者在body里面)都包含下一步request里面需要包含的信息,要花點心思研究出來。有一點要注意的是,用瀏覽器登錄的過程中可能會有幾次自動重定向,你在用腳本模擬的時候要把它取消,因為每一步http都要你顯式的發送、接收。suerperagent這樣取消自動重定向:
superagent.get(‘www.baidu.com’).redirects(0)
如果要測試的系統都是HTTPS,需要取得信任證書,並導出來(瀏覽器登錄https的時候會要求接受證書,這個過程中可以導出來),以備模擬登錄時使用。superagent使用證書簡單示例,假設已准備好的證書文件為abc.pem:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
var cert = fs.readFileSync(__dirname + '/abc.pem'); superagent.get('https://abc.com') .ca(cert) .end(function(err, res) {...});
注意node.js里面一定要設置 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 才能成功。
其他准備工作:
除了獲取系統登錄信息,有可能還需要取得一些其他准備信息,比如替換掉其中某些http的一些請求信息。舉個例子,如果所測試的http中需要用到其他單證號碼,可以先准備好,測試時進行替換。通常有兩種做法,第一是離線准備很多單證號碼保存在文件里面或者數據庫,測試的時候直接拿來使用就可以了,如果不能重復使用則需要把用過的進行標記。第二種方法是現場實時創建新的單證(也是調用HTTP來創建),然后使用它。
一般推薦使用第一種方法,這樣避免把你要測試的用例和創建單證耦合在一起(雖然實際你的系統中它們確實在同一個業務流程里面),即使創建單證的功能有問題,也不影響你當前要做的測試,測試報告也更加准確。而且這樣做可以減少這個測試框架的復雜度。
准備好了之后,一次性把剛才解析出來的http對象相關的信息替換掉,頭部里面的cookie,有些需要替換URL中的信息,有些需要替換request body里面的信息,根據需要進行。
執行測試:
准備就緒后就是進行批量調用所有的HTTP請求。
剛才提到過大部分HTTP之間沒有相互依賴,所以主要是要以並行測試為主,也就是說每一個HTTP調用后不需要等它返回就直接調用下一個,像機關槍一樣瞬間把所有請求都發出去,每個HTTP的測試都是並行的,這樣可以節省大量的時間,效率非常高,100個HTTP十幾秒就跑完了,跟selenium在GUI上跑相比,那更是一個天上一個地下了。
superagent發送接收HTTP簡單示例:
function request(httpReq, testData) { //httpReq代表一個http; testData主要是設置對http response進行校驗的黑白名單等等
return new Promise(function(resolve) { var assert = require('./assertion.js').assert, //引入自己開發的assertion模塊對http reponse進行校驗
endpoint = httpReq.req_endpoint, req_method = httpReq.req_method.toLowerCase(), req_headers = httpReq.req_headers, req_body = httpReq.req_body; superagent[req_method](endpoint) .set(req_headers) .send(req_body) .timeout(10000) .end(function(err, res) { var result = assert(httpReq, res, testData); resolve(result); }); }); }
剛才講大部分都是並行,那就是說有些是需要串行的,有些比較復雜的應用確實需要按順序執行,HTTP的調用是有先后的,否則,不能保證成功(因為異步操作不能夠保證誰先做完)。這種通常發生在創建比較復雜的單證上面,它進行分步校驗或保存,有一定的先后順序。
這個還是比較考驗node.js異步編程能力的,並行不難,反而怎樣保證幾十個上百個異步操作串行,就有點難度了,需要用到Promise或者Generator的異步控制技術。以下是Promise示例代碼。
var superagent = require('superagent'); function execute(httj, testData) { //httj是所有解析出來的http對象集合; testData是為需要的http設置的黑白名單等校驗條件 var failureCount = 0, logs = []; if (testData.serial) { //串行 var p = Promise.resolve(); for (var key in httj) { void function(k) { p = p.then(function(result) { log(result); return request(httj[k], testData); //request函數簡單包裝了superagent執行HTTP的方法,見上一段示例代碼 }); }(key); } p.then(function(result) { log(result); }); } else { //並行 var allWS = []; for (key in httj) { allWS.push(request(httj[key], testData)); } p = Promise .all(allWS) .then(function(arr) { for (var result of arr) { log(result); } }); } return p.then(function() { var isPassed, report; failureCount === 0 ? isPassed = true : isPassed = false; report = logs.sort().join(''); return { //生成最終測試結果 isPassed, report }; }); function log(result) { if (result !== undefined) { if (result.status === 'failed') { ++failureCount; } logs.push(result.info); } } }
處理並校驗結果:
對測試結果校驗是這個框架中最難的部分,花最多時間進行試驗、開發的。主要原因是系統的web service設計不太標准,不遵守REST service的設計原則。HTTP response雖說主要是json格式,但是還是挺多東西需要處理的。比如,有些里面有多余的斜杠,大括號等,需要先去掉,有些HTML嵌在json里面,有些json嵌在文本里面等等五花八門,這些都給我們的測試帶來極大的挑戰。(不得不順便吐槽一下,不考慮測試的開發不是好開發,不考慮測試的框架不是好框架比如extJs 。。。呃。。。果然舒服一點了)。
除了上述一些麻煩需要預處理外,對結果的校驗需要實現以下幾個基本功能(其中使用了最流行的斷言庫chai,可以在npm下載):
完全一致對比:每次調用返回的結果跟之前錄制的結果完全一致,這個其實是最為簡單的,使用chai的“深等於”方法進行比較,字符串和JSON對象都可以。
黑名單:排除其中的一部分字符串,或者json字段,其他的進行“深等於”對比。
白名單:只選擇其中一部分字符串,或者json字段,進行“深等於”對比。
如果返回的結果是json格式,進行黑、白名單校驗,需要先把黑、白名單里面的字段查找出來,這里需要用到對象深度查找(類似深拷貝)技術,示例代碼:
function search(obj, key) { var arr = []; for (var i in obj) { if (!obj.hasOwnProperty(i)) continue; //exclude properties from __proto__
if (i === key) { var o = new Object; o[i] = obj[i]; arr.push(o); } if (typeof obj[i] === 'object') { arr = arr.concat(search(obj[i], key)); } } return arr; }
一個HTTP如果需要進行黑名單或白名單校驗,黑白名單列表需在配置文件里設置好,否則默認進行完全一致對比。黑名單、白名單配置文件示例:
"assertion_criteria": { "baidu.com/home/display": { "whitelist": ["<title>Home</title>", "name", "貨幣"] }, "baidu.com/search": { "blacklist": ["pageInfo", "id", "時間戳"] } }
以上具體使用那種校驗,在大批量web service的回歸測試中,未必能夠,或者說未必需要提前設計好,最快捷的方法是通過幾次試跑而試驗出來:
第一次試跑由於沒有設定任何黑白名單,所以按默認進行完全一致對比,測試通過的HTTP說明基本上可以按照完全一致進行對比測試(我的項目實踐中90%都可以直接用這種方式,所以非常省事)。
對於失敗的HTTP,則必須一個一個研究,看看導致失敗的變化內容,把它們放入黑名單。如果只關心其中一部分內容的正確性,可以使用白名單方式。
多試幾輪,直到所有的測試通過。在之后的時間里如果定期運行測試,還可能會發現有些會變化的內容,繼續添加黑白名單,跑幾天之后黑白名單就可以穩定下來。
還有一個必須提的關鍵點。json數據里面經常會有數組,數組就牽涉到排序問題。實踐過程中發現有些HTTP每次返回的json數據是沒有問題的,但是它的數組的元素是對象,而且順序是隨機的,這樣就導致測試失敗。但是沒有很好的方法對這種數組進行排序,最后想到的解決方法就是把整個嵌套的json樹扁平化,即把所有的末端數據全部取出來,放到一個數組里面再進行排序,這樣可以保證數據完整,並且順序固定。示例代碼:
function flatten(obj) { var arr = []; if(obj instanceof Array) { obj.forEach(function(element) { if(typeof element!=='object') { arr.push('ROOT:' + element); } else { arr = arr.concat(flatten(element)); } }); } else { for(var key in obj) { if(!obj.hasOwnProperty(key)) continue; if(typeof obj[key]!=='object') { arr.push(key + ':' + obj[key]); } else { if(obj[key] instanceof Array) { obj[key].forEach(function(element) { if(typeof element==='object') { arr = arr.concat(flatten(element)); } else { arr.push(key + ':' + element); } }); } else { arr = arr.concat(flatten(obj[key])); } } } } arr.sort(); return arr; }
把http reponse經過上述黑白名單、扁平化處理后,就可以使用chai庫進行字符串或者json對象比較(chai的eql或者contain方法),判斷結果是否一致。這里就不貼代碼了。
測試報告:
對於測試結果的報告,主要有三件事要做。
控制台的顯示:沒有什么特別的東西,就是把所有的http的測試結果打印出來,通過的打勾,失敗的打叉,加上相應的顏色,漂亮一點(模仿Mocha,呵呵),注意對於並行測試的順序不能保證,所以顯示順序是亂的,但串行就是按照你錄制的順序顯示的。

郵件報告:在測試的過程中把每個HTTP的測試結果記錄下來,最后一次性發送郵件給相關人等。如果其中有失敗的HTTP,則在郵件標題上顯示失敗。
log文件:主要是把測試失敗的http的response記錄下來,放到文件里面,方便查錯。建議把log文件加上時間戳命名,然后把鏈接放在郵件里,不建議把log文件添加郵件附件,以防止郵件過大。

定期執行:
很多人直接把使用測試工具測試叫做自動化測試,其實不是很准確,如果次次要手動去啟動,還要盯着它跑完以防出錯,這種頂多算是半自動。真正的自動化測試必須是自己定期執行的,並且不需要太多的人工干預,只有測試失敗的時候才需要去調查。
我因為沒有太高的要求,所以把上面的過程基本上簡單的設定為半小時跑一次。根據需要你也可以實現一些定點比如每天8點,13點,20點運行的job,以避開系統版本部署的時間,造成誤報警。
總結
API測試最大的好處就是大大減少了GUI自動化測試中的開發、維護成本,執行穩定且速度極快。它的另一個好處就是數據敏感性,非常適合測試返回數據集大而復雜的場景,數據集中任何字段返回非期望結果都會馬上發現,肉眼測試是不可能做到的,如果在GUI自動化中來實現,也需要花費很大精力把數據從復雜的HTML里摳出來。
當然,每種技術都有它的強項和弱項。基於HTTP錄制回放的自動化測試,最大的限制就是不容易把測試數據分離出來(當然對於一些簡單的http請求,是可以分離的;對於非常復雜的請求一般很難做到),這就導致不同的測試數據需要重新錄制。不過由於錄制過程實際上非常簡單快捷,對於回歸測試來講,這並不是一個很大的問題。
目前我所實踐的API測試方法,主要集中在CRUD場景的測試中,暫不支持非實時返回結果的流程類場景,以后會增加相關支持。本文主要是分享HTTP層面的測試方法而非測試框架功能代碼分享,希望大家從概念和測試方法上有所收獲,具體的實現技術和代碼,可以自行選擇自己熟悉的語言。
總體上,對於表現層Web Service進行的測試既不是典型的端對端測試,也非集成或單元測試,在ThoughtWorks推崇的自動化測試金字塔理論中,它處於這個位置:

最后,照例強調一下,自動化測試不應該在回歸測試的時候才做。 #我愛TDD#