當下微服務如火如荼,各個團隊在爭先恐后推出微服務,不論在概念上還是在實踐上,如果自己沒有跟微服務掛上鈎,便會被貼上落伍
的標簽。我們在推微服務的時候,我們說微服務架構具備如下優勢:
- 架構靈活,能夠應對復雜的業務需求。
- 獨立部署,大大提高CI/CD的效率。
- 服務自治,支持技術棧多元化。
- ......
這些特征恰恰是單點應用無法具備的,因此微服務架構在廣大的呼聲下逐漸承接了單點應用的替代工作。隨着容器技術的成熟,使用Docker重建一個應用的成本趨近於零。而K8S/Rancher在生產上的廣泛應用,很大程度解決了容器管理的難題。調用鏈分析工具(ZipKin)、ELK+Kibana再配合系統監控工具(Prometheus),就連微服務架構帶來的部署運維的復雜性也得到了大大的改善。更加樂觀的是,眾多雲平台(AWS, GCP, Azure等)正在試圖打造解決部署運維難題的一體化Paas服務,讓應用開發商更加專注於業務上。
如果將軟件生命周期大致划分成兩部分:

我們認為左邊部分正在享受着微服務架構的益處,而右邊部分在遭受着微服務架構復雜性的折磨。
微服務架構帶來的復雜性(右邊部分)已經很大程度上得到了解決,常見的解決方案是在開發團隊中植入DEVOPS。比如在ThoughtWorks中的某些團隊,DEVOPS成為Team不可或缺的成分。
我們把注意力放在左邊部分。開發人員關注更多的是開發
,每個服務由一個小的Team負責開發,Team正在極力往服務自治
的方向靠攏。測試人員可能更加關注測試
,尤其是契約測試伴隨着業界對集成測試(UI測試)的痛斥聲而崛起。消費者驅動契約測試
的演講比比皆是,我也沒有例外,在某Account的技術大會上做了一次 [微服務架構下的測試應對策略]({{ site.url }}{{ '/test-strategy-to-meet-microservice-architecture/' }}) 的分享。在分享中,我趕時髦提倡用契約測試取代集成測試,但是細節中沒有忽略的一個核心點:單元測試。這也是本文我要分享的重點。
基本最無敵
單元測試是根,是基本,基本最無敵
單元測試存在於測試金字塔的底端,撐起了整個金字塔,編寫它是開發人員的職責。微服務架構讓服務更加獨立小巧,這意味着我們不用為小巧的代碼庫編寫單元測試了嗎?微服務架構提倡服務與服務之間通過契約測試來集成,這意味着我們只用編寫契約測試就足夠了嗎?
假設我們以正確的姿勢在踐行微服務的相關技術實踐。
CI上會伴隨每次提交都觸發單元測試、Service測試(API測試)、契約測試,所有測試通過后開始獨立部署,如果我們的契約測試寫的足夠好,便可以自信地獨立部署。如果Service測試覆蓋的足夠全,便可以自信地說代碼缺陷率很低。此時我們可能認為單元測試業務價值低,不必過多關注。
回到現實,實際情況可能是這樣子的。
CI上有契約測試的Stage,但也是草率編寫,甚至契約測試因為沒人維護而被默認忽略。Service測試寫了大量的Case,導致測試運行時間被拖長,Build效率大大降低。由於大量的Servcie測試的存在導致單元測試被過度輕視,再加上無效的測試充斥着代碼庫。這幾點不但扼殺了服務獨立部署 的特性,而且增加了開發部署的工作量。
再者,根據康威定律:系統架構取決於組織架構,它倆息息相關
。不少團隊懷揣微服務架構的夢想,卻在老一套組織架構的驅使下漸行漸遠。最后迫不得已,將原來一個大Team按照功能模塊拆成幾個小Team,將代碼庫粗暴地拆分成多個,每個開發人員同時往所有的代碼庫中提交代碼。
微服務架構的優勢會驅使團隊在一開始就高架微服務,無視業務需求復雜度,走一遍Event Storming,來一場DDD活動,確定幾個服務便開始搞下去。而微服務架構提倡的是演進式架構,某些團隊卻因為各種原因一直停留在起初確定的那幾個服務下開發下去,拆分的不合理性和演進性沒有得到體現。這足以扼殺微服務架構能夠應對復雜業務需求 的特性。
微服務架構本身並沒有錯。歸根結底是業務的復雜性很難被駕馭。我們說DDD可以幫助做微服務設計,於是我們都來學習Eric Evans 的DDD,可它卻不能有效解決以下幾個問題:
- 如何進行領域建模?
- 如何划分限界上下文?
- 如何在實現層面定義對象?
所以,我們學習了DDD還是不會DDD。但有一點毋庸置疑,我們每個人(DEV)都會編寫單元測試。我們在試圖駕馭微服務架構的路上摒棄了陳舊的集成測試、掌握了新的契約測試,而任何時候我們都應該始終抓住根本:編寫有效的單元測試來為我們的系統保駕護航。
三個維度看單元測試
我們不會說單元測試是靈丹妙葯,對於100%覆蓋率我們也應該持有保留態度。但在一個微服務架構基礎設施還不完善、開發人員能力參差不齊、DDD能力不足以應對復雜業務的情況下,單元測試是性價比最高的實踐。
能力建設
一個具備開發經驗的開發人員,基本上都會編寫單元測試。即便不會,可以通過培訓來快速達成。從學習曲線上看,單元測試很容易上手(方法難以被測試另當別論),擁抱Java大腿的JUnit就是一個很好的例子。所以在一個團隊中,我們可以過培訓、Pair 快速讓開發人員具備編寫單元測試能力。
測試即文檔,對於新上項目的開發人員,可以通過閱讀單元測試來了解業務需求,並且不會對一系列具備復雜數據安裝的Service測試產生恐懼感。
生產效率
在那些重Service測試而輕單元測試的項目中,Service測試里的數據安裝缺少易用的腳手架,實際上編寫出來的諸多Service測試猶如行屍走肉,不但沒有測試出缺陷,還降低了測試運行速度,拉長了反饋時間。
實踐證明,很多缺陷完全可以通過單元測試來發現,測試金字塔提出者Martin Fowler 強調 如果一個高層測試失敗了,不僅僅表明功能代碼中存在bug,還意味着單元測試的欠缺。因此,無論何時修復失敗的端到端測試,都應該同時添加相應的單元測試。 而越早發現發現Bug,造成的浪費就會越小,單元測試本身就能夠提供了快速反饋的機制。
卓越態度
追求卓越是一個優秀程序員必備的態度。優秀的程序員除了能夠編寫Clean Code,還應該能夠編寫Clean Test。而Clean Code的基本特征之一是易於測試。單元測試可以充當一個設計工具,它有助於開發人員去思考代碼結構的設計,讓代碼更加有利於測試。知名的開源代碼庫從來不會缺乏單元測試,而給與他們自信的也正是這些可觀的單元測試覆蓋率。
考慮到成本與收益比,我們不必保證100%的覆蓋率。因為隨着覆蓋率提升,單元的測試的價值越來越低,而編寫的成本卻越來越高。所以相比於100%這個漂亮的數字,我們應該去追求那不到100%的單元測試的有效性。
夯實根基
單元測試能為代碼庫保駕護航的前提是它本身應該有效可靠。
編寫單元測試的能力容易培養,但編寫有效的單元測試卻需要不斷地刻意練習,甚至一個有多年經驗的Senior開發人員也不一定能夠時刻編寫出有效的單元測試。
讓單元測試有效的一個很好的方式是盡可能讓我們的被測代碼具備良好的可測性。要做到這點,我們需要盡可能的在編碼的過程中掌握必要的代碼設計原則。就拿面向對象的編程編程語言來講,我們在編寫代碼的時候要時刻思考Robert C. Martin 提出的SOLID
原則:
- SRP(Single Responsibility Principle),單一職責原則
- OCP(Open Closed Principle),開放封閉原則
- LSP(Liskov Substitution Principle),里氏替換原則
- ISP(Interface Segregation Principle),接口分離原則
- DIP(Dependency Inversion Principle),依賴倒置原則
同時我們應該盡量避免編寫STUPID
代碼:
- Sington,單例
- Tight Coupling,緊耦合
- Untestability,不可測
- Premature Optimization,過早優化
- Indescriptive Naming,胡亂命名
- Duplication,重復代碼
在做設計和編寫代碼的時候多思考我們是不是在踐行GRASP
原則:
- Controller,控制器
- Creator,創造者
- High cohesion,高內聚
- Low coupling,低耦合
- Polymorphism,多態
- Indirection,中介
- Information expert,信息專家
- Protected Variations,受保護變化
- Pure fabrication,純虛構
以上這些原則需要在編碼中不斷地刻意練習,除了閱讀針對性的書籍,在團隊中積極組織 Code Review、推動 Pair 來互相學習和改進是一個更有效的方式。
良好的代碼設計讓我們的單元測試更加容易編寫,而要編寫有效的單元測試,我們應該對以下幾個維度的測試壞味道保持敏銳的嗅覺:
- 可讀性:基本斷言、附加細節、冗長安裝、邏輯分隔、魔法數字、過度斷言 等。
- 可維護性:重復、條件邏輯、參數化混亂、殘缺路徑、永久性臨時文件、弱不禁風 等。
- 可靠性:被注釋、歧義注釋、永不失敗、輕率承諾、降低期望、有條件的測試 等。
歡迎加入學習交流群569772982,大家一起學習交流。