前端自动化测试浅析
前言:測試簡介
前端常見的問題:
修改某個模塊功能時,其它模塊也受影響,很難快速定位bug
多人開發代碼越來越難以維護
不方便迭代,代碼無法重構
代碼質量差
增加自動化測試后:
我們為核心功能編寫測試后可以保障項目的可靠性
強迫開發者編寫更容易被測試的代碼,提高代碼質量
編寫的測試有文檔的作用,方便維護
?
測試方法可以分為三個大類黑盒測試、白盒測試、灰盒測試
黑盒測試一般也被稱為功能測試,更注重結果的展示,要求測試人員將程序看作一個整體,不考慮其內部結構和特性,只是按照期望驗證程序是否能正常工作(就是不知道源代碼是什么意思,只是針對界面、bug的測試),現在傳統的測試部門主要采用這種測試方式,比較片面化,只能測到看得的東西,一些內部復雜的邏輯可能測不到。
白盒測試是基于代碼本身的測試,一般指對代碼邏輯結構的測試,更注重數據的流動,注重過程。
灰盒測試是一種集合了白盒測試和黑盒測試的長處的測試方法。
?
相關概念
TDD
TDD是Test Driven Development 的縮寫,也就是測試驅動開發。
通常傳統軟件工程將測試描述為軟件生命周期的一個環節,并且是在編碼之后。但敏捷開發大師Kent Beck在2003年出版了 Test Driven Development By Example 一書,從而確立了測試驅動開發這個領域。
TDD需要遵循如下規則:
寫一個單元測試去描述程序的一個方面。
運行它應該會失敗,因為程序還缺少這個特性。
為這個程序添加一些盡可能簡單的代碼保證測試通過。
重構這部分代碼,直到代碼沒有重復、代碼責任清晰并且結構簡單。
持續重復這樣做,積累代碼。
TDD具有很強的目的性,在直接結果的指導下開發生產代碼,然后不斷圍繞這個目標去改進代碼,其優勢是高效和去冗余的。所以其特點應該是由需求得出測試,由測試代碼得出生產代碼。打個比方就像是自行車的兩個輪子,雖然都是在向同一個方向轉動,但是后輪是施力的,帶動車子向前,而前輪是受力的,被向前的車子帶動而轉。
BDD
所謂的BDD行為驅動開發,即Behaviour Driven Development,是一種新的敏捷開發方法。其實可以認為BDD是TDD的一個子集或分支,是測試驅動開發的升級版,它更趨向于需求,需要共同利益者的參與,強調用戶故事(User Story)和行為。2009年,在倫敦發表的“敏捷規格,BDD和極限測試交流”中,Dan North對BDD給出了如下定義:
BDD是第二代的、由外及內的、基于拉(pull)的、多方利益相關者的(stakeholder)、多種可擴展的、高自動化的敏捷方法。它描述了一個交互循環,可以具有帶有良好定義的輸出(即工作中交付的結果):已測試過的軟件。
它對TDD的理念進行了擴展,在TDD中側重點偏向開發,通過測試用例來規范約束開發者編寫出質量更高、bug更少的代碼。而BDD更加側重設計,其要求在設計測試用例的時候對系統進行定義,倡導使用通用的語言將系統的行為描述出來,將系統設計和測試用例結合起來,從而以此為驅動進行開發工作。
大致過程:
從業務的角度定義具體的,以及可衡量的目標
找到一種可以達到設定目標的、對業務最重要的那些功能的方法
然后像故事一樣描述出一個個具體可執行的行為。其描述方法基于一些通用詞匯,這些詞匯具有準確無誤的表達能力和一致的含義。例如,expect, should, assert
尋找合適語言及方法,對行為進行實現
測試人員檢驗產品運行結果是否符合預期行為。最大程度的交付出符合用戶期望的產品,避免表達不一致帶來的問題
?
測試的分類
單元測試(Unit Testing)
集成測試(Integration Testing)
端到端測試(E2E Testing)
一、單元測試(Unit Test)
參考鏈接:
https://www.jianshu.com/p/bb713d2fe3ad
1、什么是單元測試
單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。對于單元測試中單元的含義,一般來說,要根據實際情況去判定其具體含義,如C語言中單元指一個函數,Java里單元指一個類,圖形化的軟件中可以指一個窗口或一個菜單等。總的來說,單元就是人為規定的最小的被測功能模塊。單元測試是在軟件開發過程中要進行的最低級別的測試活動,軟件的獨立單元將在與程序的其他部分相隔離的情況下進行測試,屬于白盒測試。
?
對于JavaScript來說,通常也是針對函數、對象和模塊的測試
2、JavaScript單元測試現狀
單元測試在后臺開發中非常流行和普及,比如JAVA開發者的JUnit等,而在前端開發中則使用的非常少。究其原因,主要是單元測試更適用于邏輯代碼的測試,這對于JAVA等后臺編程語言來說測試起來非常方便,但是前端開發很多時候要要UI打交道,UI相關的代碼不是不可以進行單元測試,但的確很麻煩,比起邏輯代碼來說困難多了,這就導致了單元測試在前端開發沒有普及起來。
但是隨著單元測試的普及,尤其是敏捷開發的推動,涌現了許多優秀的JavaScript單元測試框架,如QUnit、Jasmine等。所有的這些框架基本上都能對Javascript代碼進行很好的測試,當然UI部分的代碼測試一樣比較麻煩,但是我們可以通過精心構造我們的測試代碼來測試部分UI代碼。
3、為什么要進行單元測試
經驗表明一個盡責的單元測試方法將會在軟件開發的某個階段發現很多的Bug,并且修改它們的成本也很低。在軟件開發的后期階段,Bug的發現并修改將會變得更加困難,并要消耗大量的時間和開發費用。無論什么時候作出修改都要進行完整的回歸測試,在生命周期中盡早地對軟件產品進行測試將使效率和質量得到最好的保證。在提供了經過測試的單元的情況下,系統集成過程將會大大地簡化。開發人員可以將精力集中在單元之間的交互作用和全局的功能實現上,而不是陷入充滿很多Bug的單元之中不能自拔。
?
4. 如何進行單元測試
4.1 選擇測試工具
在JavaScript世界中,我們需要至少三個工具來進行單元測試,這意味著每個工具都需要你進行選擇:
測試管理工具測試管理工具是用來組織和運行整個測試的工具,它能夠將測試框架、斷言庫、測試瀏覽器、測試代碼和被測試代碼組織起來,并運行被測試代碼進行測試。測試工具有很多選擇,Selenium、WebDriver/Selenium 2、Mocha[1]、JsTestDriver、HTML Runners和Karma,我這里選擇使用Karma。
karma:Google Angular 團隊寫的,功能很強大,有很多插件。可以連接真實的瀏覽器跑測試。能夠用一些測試覆蓋率統計的工具統計一下覆蓋率;或是能夠加入持續集成,提交代碼后自動跑測試用例。
?
測試框架測是框架是單元測試的核心,它提供了單元測試所需的各種API,你可以使用它們來對你的代碼進行單元測試。JavaScript的測試框架可謂百花齊放,選擇太多了(可以參考List of unit testing frameworks),我這里選擇使用Mocha(關于它們中一些框架的對比,可以參考javascript單元測試)
Jasmine:自帶斷言(assert),mock功能
Mocha:框架不帶斷言和mock功能,需要結合其他工具,由tj大神開發
Jest:由Facebook出品的測試框架,在Jasmine測試框架上演變開發而來
?
斷言庫斷言庫提供了用于描述你的具體測試的API,有了它們你的測試代碼便能簡單直接,也更為語義化,理想狀態下你甚至可以讓非開發人員來撰寫單元測試。所謂的斷言,就是預期某些執行結果符合你自己的要求。所有的測試用例都應該含有一句或多句的斷言。當然,你也完全可以不使用斷言庫,而是用自己的測試代碼去測試,不過幾乎沒有人會這么干,除非你自己實現了一個測試斷言庫。測試斷言庫的選擇也不少:better-assert、should.js、expect.js、chai.js等等(有關它們的對比,可以參考幾款前端測試斷言庫(Assertions lib)的選型總結)我這里選擇chai.js。
chai: 目前比較流行的斷言庫,支持 TDD(assert),BDD(expect、should)兩種風格
should.js:也是tj大神所寫
mock庫
參考鏈接:
https://www.jianshu.com/p/bb713d2fe3ad
sinon.js:使用Sinon,我們可以把任何JavaScript函數替換成一個測試替身。通過配置,測試替身可以完成各種各樣的任務來讓測試復雜代碼變得簡單。支持 spies, stub, fake XMLHttpRequest, Fake server, Fake time,很強大
?
有了上面的四個工具,你就可以開始對你的node代碼進行測試了。但是如果想要對前端代碼進行測試,還需要另外一個工具:
測試瀏覽器前端代碼是運行在瀏覽器中的,要對其進行單元測試,只能將其運行在瀏覽器上。目前大部分測試工具都支持調用和運行本地瀏覽器來進行測試,但如果你的測試僅僅是針對函數和模塊的單元測試,則完全可以使用一款無界面的瀏覽器:PhantomJs
另外,還有一個很重要的事情就是測試覆蓋率的統計。一般情況下你的測試管理工具會提供相關的覆蓋率統計工具,但是有些情況下它們提供的工具未必是你想要的。比如當被測試的代碼是經過了某些打包工具打包完了且被壓縮和混淆了,同時打包工具還混入了很多自己的代碼,這時覆蓋率的統計就容易不準確。所以為了避免這種情況,測試覆蓋率統計工具需要謹慎選擇,至少你得確認它支持你的打包工具已經打包好的代碼。
?
測試覆蓋率統計工具
Karma-Coverage是Karma官方提供的覆蓋率統計插件,自然成為項目的首選。
?
參考網站:
https://www.cnblogs.com/baoshuyan66/p/9937087.html
?
karma-webpack?用webpack預處理文件
karma-coverage?測試覆蓋率
karma-mocha?接入mocha測試框架
karma-spec-reporter?輸出報告
karma-phantomjs-launcher?控制PhantomJS
karma-phantomjs-shim?給PhantomJS兼容的控制
?
4.2?構建一個最基本的測試
參考網站:
https://www.jianshu.com/p/6726c0410650#fn1
?
describe('index.js的測試', function () {it('1應該是數字', function() {// expect(isNum(1)).to.be.trueisNum(1).should.equal(true)})it('"1" 應該是字符', function() {// expect(isString('1')).to.be.trueisString('1').should.equal(true)})})describe塊稱為"測試套件"(test suite),表示一組相關的測試。它是一個函數,第一個參數是測試套件的名稱("index.js的測試"),第二個參數是一個實際執行的函數。
it塊稱為"測試用例"(test case),表示一個單獨的測試,是測試的最小單位。它也是一個函數,第一個參數是測試用例的名稱("1應該是數字"),第二個參數是一個實際執行的函數。
?
其中,describe和it是mocha的語法結構,describe是這對某個組件或者函數的名字描述,測試腳本里面應該包括一個或多個describe塊,每個describe塊應該包括一個或多個it塊。it是對它需要完成某些功能的描述,它里面是具體的測試用例。在測試框架中,describe,it,?expect和sinon都是全局方法。
expect(isString('1')).to.be.true是Chai下的語法?
二、集成測試(Integration Testing)
1、什么是集成測試
?
將已測試過的單元測試函數進行組合集成暴露出的高層函數或類的封裝,對這些函數或類進行的測試,屬于白盒測試
選用Travis CI(免費)或Circle CI來完成持續集成測試
本地根目錄創建名為 .travis.yml 的Travis配置文件
?
阮一峰教程:
http://www.ruanyifeng.com/blog/2017/12/travis_ci_tutorial.html
?
三、端到端測試(E2E Testing)
參考鏈接:
https://blog.csdn.net/sinat_33312523/article/details/82955514
1、什么是端到端測試
End to End Testing, 打開應用程序模擬輸入,檢查功能以及界面是否正確,像是一個自動化的測試腳本,模擬用戶行為。在Web應用程序中,他們會啟動服務器,打開瀏覽器,去自動點擊一個真實瀏覽器環境中的頁面,再通過直接抓取頁面上的DOM來斷言是否符合預期,這是最接近用戶的測試方式,從一定程度而言端到端測試對于一個產品的發布起到了至關重要的作用,這直接關系到了DOM給用戶帶來的視覺上的交互。不同于行為驅動測試(BDD)和單元測試獨立運行并使用模擬/存根,端到端測試將試著盡可能從用戶的視角,對真實系統的訪問行為進行仿真。對Web應用來說,這意味著需要打開瀏覽器、加載頁面、運行JavaScript,以及進行與DOM交互等操作。屬于灰盒測試
2、框架介紹
官網:https://www.cypress.io/
Cypress是基于 electron 的一個測試框架,提供 web 環境進行點對點的測試,在 programer 思維下,進行自動化的交互操作,必要點檢測說明,這是一個非常棒的用處。例如之后擁有數據埋點,可以在固定的位置檢測是否有埋點。
?
2.1 Cypress四個特性
?
時間旅行
Cypress在您的測試運行時拍攝快照。只需將鼠標懸停在命令日志中的命令上即可確切了解每一步中發生的情況。不僅僅是每個測試用例都會出現在測試頁面的左側,每一次瀏覽器的行為都會被記錄在一個類似Timeline的列表中,在鼠標移入后右側頁面將會停留在一個那一個瞬間形成一個快照庫,我認為這是一個非常棒的功能,這更適合我們來定位bug所發生的時間點和位置。
?
可調試
不用再去猜測為什么你的調試失敗了。因為這款框架是基于electron來編寫的,所以用來承載它運行的為Chrome瀏覽器,也就是說我們的應用是跑在一個真實的瀏覽器的環境中,所以在我們的頁面出現問題的時候,我們同樣可以調用熟悉的F12控制臺去debug
?
實時重新加載
不論你何時更改了測試內容,Cypress都會自動熱加載,這樣你就能隨時在應用中看到所執行的命令
?
自動等待
Cypress會自動等待命令和斷言。這也是一個非常討喜的功能,在使用Jest中,我時常需要調用Vue自身所攜帶的異步等待函數vue.nextTick來進行下一步的斷言。在Cypress中我們完全不需要擔心異步等待在代碼中的呈現,框架會自動等待回調的結束,在這個過程中并且會啟動一個計時。
?
2.2 Cypress配置安裝
目錄結構
?
在根目錄下還需要一個cypress.json
{"pluginsFile": "tests/e2e/plugins/index.js","projectId": "8efjtr" }?
我們這里一個個來介紹文件的目錄結構和配置說明
?
cypress.json
首先在啟動整個Cypress時,會在項目的根目錄中去尋找這個文件,在vue-cli-3中單獨把pluginsFile這個配置項指向了我們重新配置的路徑(實際在安裝完一個Cypress項目時,測試用例所在的目錄就是cypress文件夾),在這個被指向的配置文件中再去使用config參數配置其他目錄所在的位置:
//?https://docs.cypress.io/guides/guides/plugins-guide.html module.exports = (on, config) => {return Object.assign({}, config, {// baseUrl: "http://localhost:8080", // 測試域名fixturesFolder: 'tests/e2e/fixtures', integrationFolder: 'tests/e2e/specs', // 測試文件文件夾screenshotsFolder: 'tests/e2e/screenshots', // 屏幕快照// videoRecording: true,videosFolder: 'tests/e2e/videos', // 錄制后的文件夾supportFile: 'tests/e2e/support/index.js'}) }當然我們也可以直接在cypress.json中去指定這些配置:
{"projectId": "by9ntm","fixturesFolder": "test/e2e/fixtures","integrationFolder": "test/e2e/specs","pluginsFile": "test/e2e/plugins","screenshotsFolder": "test/e2e/screenshots","supportFile": "test/e2e/support","videosFolder": "test/e2e/videos","baseUrl": "http://localhost:8080" }生成配置后可以在項目啟動后的dashboard的setting選項中看到所有我們對配置的改動,并會通過不同顏色進行標注。同樣這個json的文件還可以對不同環境下的配置做不同的更改,具體參照官方文檔。
?
config
在這個文件夾中我主要做了一個當前環境的判斷配置,由于Cypress是瀏覽器真實去訪問一個鏈接的地址,所以需要把整個項目給啟動后才可以去測試,所以需要一個準確的baseUrl
fixtures
這個文件夾很有意思,是為了存放模擬上傳或讀取的文件,在場景中我們經常會碰到文件的拖拽上傳或者下載等等,這些操作在單元測試是非常難以測試的,所以我們需要在端到端測試中把這些操作模擬掉,這個文件夾就是用來存放這類特殊文件的。
specs
顧名思義所有的測試用例都要放在這個下面。
support
還是回到上傳文件這個場景,很多特殊場景的情況下Cypress的API很可能沒有覆蓋到,所以可以在這個文件夾下配置自定義的命令全局注入到框架中使用。
?
基礎配置
/plugins/index.js
// https://docs.cypress.io/guides/guides/plugins-guide.html module.exports = (on, config) => {return Object.assign({}, config, {// baseUrl: "http://localhost:8080", // 測試域名fixturesFolder: 'tests/e2e/fixtures', integrationFolder: 'tests/e2e/specs', // 測試文件文件夾screenshotsFolder: 'tests/e2e/screenshots', // 屏幕快照// videoRecording: true,videosFolder: 'tests/e2e/videos', // 錄制后的文件夾supportFile: 'tests/e2e/support/index.js',viewportHeight: 768, // 測試瀏覽器視口高度 viewportWidth: 1366 // 測試瀏覽器視口寬度}) }運行測試
在vue-cli 3.0的默認配置中,我們直接運行npm run test:e2e來啟動我們的測試,在這之前我們需要先啟動我們的項目和mock服務,最后去啟動我們的E2E,否則baseUrl會請求不到正確的端口。
這是啟動頁面,頂部會有三個選項配置可供選擇,在</>Tests選項中我們可以看到我們所有的spec,當然這個順序是按照字母排列的,我們可以點擊Run all specs按鈕來一次性啟動所有的用例,也可以點擊單個用例來啟動。
?
?
在Runs選項中我們可以看到和CI集成所生成的項目ID
?
在Setting選項中我們可以看到merge之后所有的配置
?
運行頁面
這是一次成功的測試,可以看到左邊的列表都已經呈現一個綠色的狀態,鼠標選中可以看到當時頁面的一個狀態。
?
2.3 語法實戰
在我們寫端到端測試之前,我們應該明確我們是基于一個用戶的角度去測試我們的頁面,所以這無關我們的所有源碼,我們應該只專注于瀏覽器所呈現給我們的資源,包括頁面上的element、控制臺中network中的所有的請求以及導航欄上的url信息,這是我們可以去測試和觀察到的所有的點。
spec基本結構
?
這里舉一個最簡單的例子,和單元測試一樣,首先要把所有的用例包裹在一個describe中
?
1、在用例中先用cy.visit()方法訪問地址,這里后面只加了/是因為baseUrl已經設置過了的原因。
2、使用cy.contains()或者cy.get()去抓取DOM并進行斷言,Cypress中默認包含的斷言庫為Chai。
3、由于設有異步等待的機制,所以我們可以毫無顧及地去寫下一步的操作,包括button的點擊事件和跳轉之后url的判斷。
?
生命周期
在一個測試集合中,我們也可以加入自身的生命周期,這些生命周期主要是針對每個測試用例來執行的,包括beforeEach、beforeAll、afterEach、afterAll
我在這個測試集合中主要用到了beforeEach這個聲明周期,在每個測試用例開始之前我都對我需要的DOM進行抓取并取一個別名,這樣我方便其他用例需要時就不需要再反復去尋找這個節點對象了。
beforeEach(() => {cy.visit('/#/steps/selectMode')cy.get('.one__item__right').eq(0).find('.item__right__btn').eq(0).as('hasConfigFile')cy.get('.one__item__right').eq(0).find('.item__right__btn').eq(1).as('notConfigFile')cy.get('.one__item__right').eq(1).find('.item__right__btn').eq(0).as('hasSystem')cy.get('.one__item__right').eq(1).find('.item__right__btn').eq(1).as('notSystem')cy.get('.btn__next').as('next')cy.get('.item__upload__text').as('fileName')})在取了別名之后其他用例只需要調用cy.get('@name')就可以取到相應別名的DOM元素。
模擬請求
在我們測試的時候總是會免不了一些請求的發出,在Cypress中由于是真實的瀏覽器環境,所以所有的請求都會被正常發出,但是有些時候我們需要mock掉一些請求來觀察DOM的反饋是否符合預期,這里就需要引入一個比較重要的概念——存根stub。
不同于單元測試的mock,我認為在單元測試中更類似于axios中的攔截器,對整個請求的代碼層面進行一個攔截后返回一個相同格式的對象騙過,而在端到端測試中因為我們無法對項目本身的源碼下手,所以我們只能從瀏覽器層面去模擬,在這里的存根我的理解是在頁面發出請求之前,先對一個API做一個標記,當瀏覽器觸發這個方法并發送請求后使用標記后的模擬請求返回并進行后續的斷言操作,我們來看一下代碼。
describe('installSystem', () => {it('尋找節點失敗', () => {cy.server()cy.route({method: 'DELETE',url: 'api/find/node',status: 200,response: {data: {},error_code: 1,message: 'error'}})cy.visit('/#/steps/installSystem')cy.wait(1000)cy.get('.pop_content_confirm').find('div').find('div').contains('尋找節點出錯')}) })在這個例子中,由于請求在頁面剛被掛載后就被觸發了,也就是說整個請求是寫在mounted這個聲明周期中的,所以我們需要在訪問頁面之前就對這個需要被mock的api做一個stub。
?
1、首先我們使用cy.server()聲明一個mock的請求。
2、然后使用cy.route()去描述我們需要模擬的api的具體信息,在里面可以填寫很多的配置,包括請求的方法method,請求的地址url,請求返回的狀態碼status以及最后返回的response body,在這里由于項目本身中還定義了error_code狀態碼,所以對于這一個請求所具備的狀態我們就需要寫很多個測試用例的組合去斷言是否符合我們的預期。
3、然后因為請求已經被我們存根了,我們再去使用cy.visit()訪問一次頁面就可以看到我們所需要被模擬的請求已經被存根并且成功模擬了。
?
?
到目前為止我們就已經非常成功地對一個API進行模擬請求了,對比起單元測試還是方便了不少的。
總結
- 上一篇: 纯CSS实现React Logo图形,内
- 下一篇: 【重学JS系列】slice用法大合集