所在位置:首頁 -- 質量管理 -- 正文

持續集成 測試開發之路--持續集成


發布時間:2017-4-1  來源:admin

 前言

如果說在工作里有個事情一直在顛覆我的認知,那就是CI了。每個階段我對它的理解都是不同的,每個階段我都會這么想:恩,CI也就是這樣了。但是項目發展一段時間以后我就又會想:哦,原來CI比我以前想的難的多。我相信在項目中實踐完美的CI方案是每個QA的理想,可惜它就好似別人家的孩子一樣,是我們心中的痛。所以今天我們調侃一下CI吧。本篇文章假設讀者已經清楚的知道CI的概念和優點。所以就不在過多的介紹概念性的東西了。

工程文化

CI需要工程文化的支撐,它不是某個人某個職位的事情,它需要整個技術團隊所有人的努力,是一種所有人都為產品質量努力的工程文化。對于QA來說,在推動CI的最初,也就是說服所有人CI的重要性以及澄清CI的形式是比較重要也比較難的。因為很多人的傳統思想都是說測試是QA的事情,或者說大家都忙于加班,哪有時間寫UT,又或者說本身就對CI這個事情有所質疑,甚至不知道CI是什么也時有發生,所這個時候澄清CI這件事本身就是比較重要的。需要讓團隊中的所有人慢慢轉變他們的思想和工作方式。可能首先是先勸服項目經理和開發的老大,然后慢慢的去推動這件事。如果你已經處于一個良好的工程文化下,那么恭喜你,你已經走過了最艱苦的第一段路。如果你說服不了他們,或者成果比較小,過程也比較慢。那其實我們自己也是有一些事情可以做的。先從QA這里把CI的架子搭起來,營造一個良好的質量的生態環境后。 別人看到了CI的樣子,是有助于他們理解你工作的重要性的。所以我們今天大部分介紹的都是QA做的事情。

高度自動化

想要做好CI就得高度自動化,這個大家都沒意見。我們需要自動化編譯,部署,運行單元測試,集成測試,功能測試。 只可惜單元測試這個最重要的組成部分在我們國內是很難推的。我相信大家都熟知下面這個金字塔形的自動化體系。

可惜理想是豐滿的,現實是骨感的。單測在我們國內的工程文化中并不吃得開。所以往往我們做的都是一個倒過來的金字塔。這就在運行時間,穩定性和覆蓋率方面有了極大的挑戰。所以各位小伙伴,碰到肯在UT上下功夫的團隊就要珍惜啊~(感謝第四范式的RD小伙伴,UT真的挺上心的)。雖然大部分的團隊情況并不理想,但是該做的還是得做,我們還是可以在UI自動化和接口自動化上動手腳。

技術選型

這個可說的不多,無非就是市面上一些典型的工具和框架。我介紹下我們公司用的。

環境搭建方面:docker驅動,可以看一下我之前發的環境管理相關的文章。

單元測試方面(Java):

Junit:本來想用我熟悉的testng,但是開發的同學說測試springmvc只能用Junit。所以只能這樣了

mockito:大名鼎鼎的java mock框架。解耦,提高覆蓋率,行為測試的神器。

mockmvc:想測試springMVC的controller的話,只能用這玩意了

hsqldb:java的memoryDB,能夠模擬真實的數據庫,但是運行在內存中。單元測試的不二神器,提高運行速度,跟真實環境解耦。

jacoco:java的代碼覆蓋率神器。

單元測試方面(scala)

scala test: 相當于java的 junit,但同時把斷言,mockito等等功能都集成進來了,是一個大的測試包

spark test:因為我們只用scala處理spark,所以不涉及到數據庫,但是涉及到了spark的測試。這是個開源的spark測試項目,里面可以幫助你啟動local mode的spark,并提供了一些RDD,DF等等的斷言工具。缺點是運行的速度仍然不夠快。

接口測試方面:

rest-assrued:代替http,它的api和獨創的斷言機制很贊

assertJ:java 斷言神器,db的斷言基本全靠它了。同時自動化測試中的數據恢復機制也是基于它實現的

testng:這個不用說了

allure report:高大上的report框架

還有一些小的,我自己封裝的東西就不說了。

UI自動化方面:

selenide:基于webdriver的測試框架

其他的跟接口測試差不多。

分支策略

說起CI,那就離不開分支模型。一個好的分支模型是做CI的基礎。我先來描述一下我見過的分支模型吧。

單分支模型

在軟件開發中,持續集成能夠解決的問題是盡早的集成和盡早的反饋。因此,盡管目前流行的所有版本控制工具都提供了分支/合并功能,但在團隊和項目都比較小的時候,還是使用單分支開發策略是比較適合做CI的。這樣會減少多分支在合并時的開銷,也減少了CI消耗的資源。這時候CI的模式圖大概是下面這個樣子(一個朋友的圖,我自己懶得畫了)

這個是比較理想的模式,RD在check out代碼后進行開發,開發完成后在本地自動化運行測試。證明新開發的功能在本地是沒有問題的。但是在這之前其他的RD不停的往分支上push代碼。所以這時候才需要從分支中在將最新的代碼merge回本地再進行一次自動化測試以保證自己的代碼不會跟最新的代碼沖突。最后push到分支上,分支的代碼配置了觸發式的自動化測試。當監控到有新的代碼更新,自動化部署和測試就會運行起來做最后一道檢查。

我來解釋一下上面的行為,可能現在有人會覺得困惑。尤其是在拉取最新代碼到本地運行一次自動化測試后,到了主干上還要做一輪測試。主要有以下幾個原因

本地環境和測試環境不一致,導致有些case在本地沒問題,但放到server上就會出問題了。

運行測試后RD有時會忘記push某個文件的情況,需要在分支上做最后一道保險

現實的情況是依靠人為的自覺很難保證每一次都按約定運行測試,所以要求程序做最后一道驗證。

分析一下

這時候對于自動化最大的壓力在于應變的速度,尤其是環境部署架構的應變速度,因為所有代碼提交都發生在一個分支上,變化會比較快,相較下來沒有緩沖時間,構建失敗影響的幾乎是所有人。而且選擇單分支模型的項目很多都還在快速發展期,產品在業務上和架構上變化會比較多。所以這時候對自動化的應變能力有很大的要求。不僅僅是測試,還有編譯和部署。我們需要在架構上把自動化設計的比較好,不能開發那邊隨便改了一個地方,你這邊就要改一大堆。那樣跟本改不過來。如果你沒自信做到好的應變,就只自動化那些最穩定的模塊。別把時間浪費在都修改腳本的事情上。

上面就是CI的一個簡短流程。不論是我們現在的單分支模型還是以后多分支模式開發。針對某一個分支的流程差不多都是這樣的。其實在外企中還經常用到一個概念叫令牌,不過我不詳細描述了,因為我實在沒怎么實踐過這個東西,只是當年外派到外企的時候用過,不知道是否合適用到我們這的環境下。這種單分支模型是初創公司常用的模式。

多分支多版本模式

每個公司的開發模式都不一樣,我介紹一下我們公司玩的模式。下面是分支模型圖。

這是我們當初的分支模型圖,我們目前的行為跟圖上略有區別,但是區別不大。其實挺好理解的,我們沒搞那么復雜。把develop分支當成主要的開發分支。 在develop分支的基礎上拉出多個feature分支,以feature為核心各自組成feature team。每個feature 分支其實就可以當做一個單分支開發模型了,feature開發并測試完成后merge回develop。但是在開發中我們要頻繁的從develop上merge代碼到feature分支上。以盡早發現feature分支跟develop上功能沖突的情況,也是為了盡早發現bug。只有通過了CI的代碼才準許合并回develop。這么做我覺得主要有以下幾個好處。

最大程度的保證了develop分支的代碼質量。代碼都是經過測試才合并到develop分支的。

把各feature team之間的互相影響降到了最低。每個feature都在自己的分支上工作。

版本發布與feature開發解耦,現在他們是兩條線。單分支開發模型中feature開發和版本發布是綁定在一起的。某些重構或者周期較長或者暫時不打算發布的feature可以在獨立的分支上開發。

分析一下

這種多分支開發模型會對CI帶來比較大的負擔,分析一下有以下幾點。

測試代碼跟隨開發走多分支模型:大多數公司的測試代碼都是單分支模型,但如果想做CI就必須跟隨開發的分支策略。因為feature分支和develop分支的行為是不一樣的,同一套代碼不能運行在這兩個分支里。需要跟開發一樣單獨開辟一個feature分支并在這個分支上寫測試代碼,結束后合并回develop。總歸一句話,一切跟著開發走。

部署同樣多分支模型:原因跟上面的一樣,不過發生的頻率很低,畢竟這個階段部署方式不會頻繁變化。

資源消耗:多分支模型,在每個分支上都要做CI。這對測試資源是有一定要求的。

測試代碼的質量要求:單分支模型的時候要求測試代碼應變要快,多分支模型的時候不僅要快,更要穩。 也就是說測試代碼是穩定的,不能總隨機性的失敗。例如我們知道UI自動化是以不穩定聞名的,這時候你要用各種手段把他們穩定下來。不穩定的不要加到CI里。因為這時候同時存在很多分支,每天都運行很多次CI。出現錯誤你要一個一個去排查,錯誤多了會讓人發瘋的。以前單分支模型的時候跑的次數少,可能一天就一次,排查的壓力小。現在這個問題被擴大了數倍。所以一定要穩。

對QA能力的要求:除了寫代碼以外,合并分支,解決沖突,code review,該走的正規流程可能都要走。多分支模型會把QA們打散到各個feature team里,也可能一個QA對著幾個feature team。而且要求所有CI問題都在feature分支上就解決掉,所以這時候對QA們的平均水平有所要求,自己水平不行指望別人幫你的情況,最好還是別發生。

分支爆炸:像我們公司這種to b的業務。會有很多個release分支,這些release分支跟那些feature 分支不一樣,他們很可能一直都不會被銷毀。因為一個分支對應一個發布版本,對應著一批客戶。不是每個客戶都愿意升級版本的。所以如果出了問題可能要在release分支下追加fix來為客戶解決問題。所以測試代碼你也要保留這個release分支以防萬一。so,分支爆炸的情況也挺煩的

恩,可以看出來 ,這時候測試的壓力比較大,但沒辦法,這是CI的代價。都是為了盡早暴露問題,盡早反饋。 我們也可以根據情況只維持一個develop的CI。這樣做的代價很小。但是有些問題都會很晚才暴露出來,這對發布版本來說就不是什么好事了,大家根據自己項目的情況抉擇吧。

多模塊多產品線模式

這個我簡單說一下吧,因為其實沒啥本質的變化。就是產品可能按模塊,甚至按產品線劃分了團隊,代碼都是放在不同的庫里。 這很常見的,針對不同的模塊,產品線做CI就可以了。 只是記得需要把整個產品都部署起來做測試也加進CI里,以證明他們相互合作是沒問題的。這個我就不多說了。

分支模型總結

各家的分支模型都不一樣,CI的方式也不一樣。也許有比我們更高效的方式,但我確實沒實踐過。所以我只能介紹下我們做CI的方式,權當拋磚引玉了。

CI的執行機制

監控觸發:監控到代碼提交就觸發一次構建,單元測試都用這種方式,因為單元測試運行快。

定時觸發:每隔一段時間觸發一次,對于運行比較慢的UI和接口測試,一般使用這種方式。

手動觸發:在Jenkins或類似的平臺上配置好任務,可能準許認為的手動觸發一次。 例如開發想push代碼前在自己的環境上跑跑冒煙測試。

一般都是3種方式配合著使用,這方面沒什么太好說的。

細節決定成敗

我們說想做好CI是比較難的,因為它是一系列細節的集合。想做好它就要把很多很多細節做好。例如流程,溝通,代碼。想做好CI就不是單獨QA一個角色能做好的。很多時候CI推不起來都是因為團隊的工程文化導致的。例如RD不想寫單元測試,或者大家覺得CI沒必要,或者壓根就沒什么好的分支模型,有些時候QA確實挺無奈的。上面說的分支模型和流程是我們團隊歷經1年多的演變而來,這里面大家的功勞很大,好多東西是RD和產品人員自發推進的,我很感謝他們給了我這么有利的環境,感謝第四范式的小伙伴們。

好了,書歸正傳,雖然我們在現實中會碰到很多困難,但是我們自己也是可以做一些事情的。我們說細節決定CI的成敗,我來說說從QA的角度哪些細節需要注意的,介紹一些小技巧。

自動化測試中的應變和維穩

前面說應變和維穩很重要,我們不希望在CI中頻繁的修改大量腳本,那樣會讓人崩潰的。很多地方的自動化到最后都處于崩潰狀態就是因為實在維護不過來了。我見過幾千條腳本每次CI失敗個幾十一百的情況。這時候CI已經是負擔了,里面有腳本不穩定的因素,也有開發做個小改動,這邊失敗了幾十個case需要一個個去改的情況。所以我來說說自動化測試中應變和維穩的小技巧吧。

封裝一切可能的可控的變化因素

我們最常見的可控制的變化因素有哪些呢?以UI自動化為例

參數個數的變化,數據庫中和頁面上各種error code的變化,error message的文案變化,各種task,訂單等等的status的定義的變化等等。

頁面元素結構的變化,屬性的變化,新增減元素的變化,各類元素的文案變化等等。

利用好枚舉

首先,把什么error code,status code,error message 的都封裝成枚舉,其實開發也是這么做的,沒準哪天他們重構的時候這些就都變了。如下:

嚴格規定所有需要用到這些概念的時候都使用枚舉,在設計方法的時候就把枚舉當做參數傳遞,或者定義一個類的時候把屬性規定為枚舉類型。總之,強制使用枚舉。這樣在變化的時候你只改這個枚舉就可以了。

page object 老生重談

咳咳,說的爛大街的事了,而且沒什么難的。但我這里還是強調一下。首先page object大家肯定都做過這件事,把每個page的元素查詢方式都頂一個專門的page class里面。但是這樣還不夠,我們要把通用的操作都放在一個方法里。如下:

我把頁面操作按算子劃分,所有想設置模型預測這個算 子的操作必須調用這個方法。可以看到里面有很多判斷,是否要自定義字段,是否全選等。就是這個算子的所有操作路徑都在這個方法里。因為分成多個方法難免代碼重復,代碼重復難免增加維護成本。大家看到我用的參數是一個對象類型,而不是基本類型。也許很多人的習慣是用String,int等等這些基本數據類型搞一個方法。但這種設計方式在遇到頁面刪減元素的時候就悲劇了,假如新加一個必填的元素,你要在這個方法里也加一個參數來控制這個元素。但以前的case還是按老的方法簽名設置的調用。所以所有調用這個方法的case都會改。 而如果你定義了一個對象當做參數,這時候只需要在對象中增加一個屬性,然后給個默認值就行了。這樣很多case其實不用改。看一下這個對象的定義:

這個對象中還使用了枚舉,我們看一下枚舉的定義:

因為控件是按文案搜的,也就是text(),所以把文案也封裝成枚舉,以防設計人員哪天看這個文案不滿意要換一個。這就是我之前說的封裝一切可控的變化。只要這個東西是多次使用的,就封裝起來。當然這里其實也可以不用枚舉,只是我習慣了。

如果某些功能,例如一些懸浮層是對所有頁面生效的,或者一些操作是對所有頁面生效的。可以把它放在base page里面,其他的page繼承這個page。如下:

為了穩定使盡一切手段

stager

這是我跟老外學的,老外管這種方式叫stager,我理解就是后門。意思是使用一些其他的手段幫助測試把前置條件準備好。 還是以UI自動化為例,假如我要測試批注合同的功能,我需要先創建合同吧,不僅是評論,查詢,刪除,修改,狀態轉變等等都需要一個合同作為前置條件。如果都在頁面上操作這個重復就太多了,創建一個合同可能要跳轉N個頁面,這不僅耗時,而且頁面操作是有不穩定因素的,容易失敗。如果前端頁面出現bug也同樣會導致一大批的case失敗。 所以老外搞出了stager這個方式。跳過頁面,調用產品代碼或者直接訪問數據庫等方式制作一些通用的服務。幫助腳本創建這些前置條件。 他們的理念是我們專們有case校驗這個頁面,沒必要讓所有腳本都走這個頁面,那太耗時了。所以那時候他們的自動化有兩種,一種叫BQ,意思是短小的case隊列,不使用stager,專門校驗頁面,邏輯簡單。另一種叫LQ,意思是長的隊列,里面都是復雜的case,使用stager創建前置條件。

數據庫操作和文件系統的操作做輔助

1.不管什么類型的測試,有些時候光檢驗接口返回值,或者頁面返回信息是不靠譜的,驗證不完全的。如果是UI測試你想要驗證結果可能做不到或者要跳轉很多頁面。這時候也許直接驗證數據庫和文件系統反而比較靠譜。

2.有些接口是異步的,你需要等待它執行完畢后去驗證結果。可你從接口返回值那里得不到執行信息,因為它是異步的。同樣頁面上你也得不到信息,或者想得到你就得頻繁的刷新,或者又是跳轉好幾個頁面。這時候還是直接寫一段邏輯輪循數據庫的狀態比較靠譜。例如下面的樣子:

再舉一個輪詢hadoop 集群任務的例子

頁面的穩定

UI自動化中,再怎么用stager你也避免不了太多的頁面操作。所以有些時候網卡一下,系統處理速度太慢或太快都有可能導致UI自動化的失敗。所以在寫page object的時候要慎重處理頁面的操作。盡量避免硬等待,使用框架的輪詢方法,如果出現錯誤話,重試一次等等。舉個例子如下:

數據的穩定

我們在自動化中難免碰到case之間會有數據的沖突。例如某條case修改了產品的全局設置,某條case在會清理掉所有的數據,某條case在運行前必須要求數據是空的等等。一般我們都會在測試的before或者after方法里小心翼翼的做數據的清理。但仍然免不了想清理的數據清理不了。那么怎么辦呢,如果我們有一種方式能自動的清理數據,而且可以讓編寫腳本的人選擇清理什么數據,什么時候清理數據。那就省事很多了。如下:

這里我定義了一個注解,注解的數據恢復策略選擇的是method,意思是每運行一個case后,都把數據恢復成case執行之前的狀態。還有一個級別是class,意思是等當前class中所有的case都運行結束后,把數據恢復到這些case執行前的狀態。這樣可以保證不會因為當前case的執行會產生或改變什么數據會影響到之后的case。當然也可以事先創造一批測試數據,例如訂單,之后針對這個訂單的任何操作都加上這個注解。這樣所有case都可以使用這個訂單測試而不擔心會被其他case影響。具體的實現方式大家翻一下我之前寫的一篇關于assertJ的用法監控式數據管理

代碼復用的細節

上面介紹了應變和維穩的細節,現在我們談談代碼復用。代碼復用的越好,我們的維護成本越少。我介紹兩個小技巧。

數據驅動

測試用例要做成數據驅動這個大家都知道,這樣可以減少代碼重復。 所以大家一般都會把測試數據都放在一個文件里。 這里需要注意一點的就是我們剛才講的我們為了應變封裝了很多枚舉和對象。但從文件中讀取出來的東西都是基本數據類型。每次讀取出來再封裝成對象這個成本太高了。 所以我是這么做的

我從文件中讀取出來的時候就轉換成對象類型了。文件中定義了很多組參數。這樣很多的case我就用一條腳本執行了。并且我不需要做類型轉換。 具體的實現細節請大家翻翻我以前的一篇關于數據驅動極其變種的文章吧。

斷言定制

在測試中還有一個有很大代碼量的地方就是斷言,在接口測試中,尤其是RPC接口測試中,我們的斷言會比較復雜。因為返回值復雜,以前測試dubbo接口的時候,發回的java對象特別復雜,一個元素一個元素取出來做斷言那太痛苦了。如果你轉成json做對比,又處理不了很多情況。假如某些字段是隨機生成的,每次都不一樣。或者說json太長做字符串對比,報錯信息很難讓你分清是哪個字段錯誤的。 所以我覺得做成下面這個樣子比較好。

我以前寫的代碼了,截個圖,上面就是比較兩個task對象是否相等,這是一個copy task的接口,我們要比較兩個從數據庫中查詢出來的task 對象是否相等,因為是用mybatis查詢出來的,所以都封裝成了對象,第一行代碼是創建一個責任鏈對象,第二行代碼規定了什么東西不需要驗證,因為task的ID是隨機生成的不可能相等。最后我們把兩個對象仍進去就行了。你不用管它怎么驗證的,責任鏈在運行到javaBean類型的時候,就會用java反射解析兩個對象的每一個屬性并調用鏈表中其他的節點做相應的斷言。不僅僅是javaBean類型,JSON,數組,List,Map,File你全都能不管三七二十一的仍進責任鏈里。以前我們最怕的就是一個ORM映射出來的字段百八十個的,光是寫斷言就寫到手軟。現在完全木有這個問題了。如果有新的驗證類型出現,你只需要在責任鏈表里增加一個節點對應這個類型作驗證就好了。我在測試開發之路----可讀性,可維護性,可擴展性中寫過實現方式。大家可以看一下。

#總結

CI中我們說工程文化是靈魂,流程是核心之一,分支模型是核心之二,自動化是核心之三。我分享了一些我做CI中的一些經驗和小技巧。其實CI還有很多方面,例如CI工具的使用配置(如Jenkins),一些輔助工具的使用(如代碼覆蓋率),CI 就是個一堆細節堆砌出來的東西,做好所有細節就才能好CI。 我們做的還有很多不足,希望以后能慢慢做的更好吧。也希望我這篇文章能引出更多的對CI的實踐的討論。

中国北京单场足球彩票