「測試金字塔」是一個隱喻,它告訴咱們將軟件測試分紅不一樣顆粒度的桶,也給出了咱們應該在這些組中進行多少次測試的想法。儘管測試金字塔的概念已經存在了一段時間,但團隊仍然很難正確地實施。本文從新探討了測試金字塔的原始概念,並展現瞭如何將其付諸實踐。討論你應該在金字塔的不一樣層次上尋找哪一種類型的測試,並給出瞭如何實現這些測試的實例。javascript
Ham Vocke前端
生產就緒軟件在投入生產以前須要進行測試。vue
隨着軟件開發規律的成熟,軟件測試方法也日趨成熟。開發團隊再也不須要大量的手動軟件測試人員,而是將測試工做最大部分自動化。自動化測試可讓團隊在短期內知道他們的軟件有什麼問題,而不是幾天或幾周。java
由自動化測試助力的縮短反饋環路與敏捷開發實踐,持續交付和DevOps文化攜手並進。 採用有效的軟件測試方法,團隊能夠快速而自信地行動。react
本文探討了全面的測試組合應該是什麼樣的響應,可靠和可維護的-不管你是在構建微服務架構,移動應用仍是物聯網生態系統。咱們還將詳細介紹構建有效和可讀的自動化測試。git
軟件已成爲咱們生活的世界的重要組成部分。它早已超出了提升企業效率這一個目的。今天,公司都試圖千方百計成爲一流的數字公司。隨着咱們每一個人都會與愈來愈多的軟件進行交互。創新的車輪會轉得更快。github
若是你想跟上步伐,必須研究如何在不犧牲質量的狀況下更快地交付你的軟件。持續交付是一種自動確保你的軟件能夠隨時發佈到生產環境中的方式,能夠爲你提供幫助。經過持續交付,能夠使用構建管道自動測試軟件並將其部署到測試和生產環境中。web
手動構建,測試和部署不斷增長的軟件數量很快就變得不可能了-除非你但願將全部時間花費在手動,重複性的工做而不是提升交付效率的工做上。算法
Figure 1: Use build pipelines to automatically and reliably get your software into productionspring
傳統上,軟件測試過於手動化,經過將應用程序部署到測試環境,而後執行一些黑盒測試,例如,經過點擊你的用戶界面來查看是否有任何問題。這些測試一般由測試腳本指定,以確保測試人員可以進行一致性檢查。
很明顯,手動測試全部更改很是耗時,重複且乏味。重複是無聊的,無聊會致使錯誤,並使你在本週末以前尋找不一樣的工做。(譯者注:意思就是沒作完就找不一樣的事作)
幸運的是,對於重複性任務有一種補救措施:自動化。
自動化重複性測試能夠成爲軟件開發人員生活中的重大改變。使自動化測試,你再也不須要盲目地遵循點擊協議來檢查你的軟件是否仍能正常工做。自動化你的測試,你能夠改變代碼庫而不用打眼球。若是你曾經嘗試過在沒有適當的測試套件的狀況下進行大規模的重構,我敢打賭這會是一個多麼可怕的體驗。 你怎麼知道你是否意外地破壞了某些東西?那麼,就是你點擊全部的手動測試用例。但說實話:你真的喜歡這樣嗎?
如何作大規模的變化的時候,並知道你是否在幾秒鐘內破壞了東西,同時喝一口咖啡?若是你問我,這樣會更愉快。
若是你想認真對待軟件的自動化測試,應該瞭解一個關鍵概念:測試金字塔。 邁克·科恩在他的着做「與敏捷成功」一書中提出了這個概念。這是一個偉大的視覺隱喻,告訴你思考不一樣層次的測試。它還會告訴你在每一個圖層上要作多少測試。
Figure 2: The Test Pyramid
Mike Cohn的原始測試金字塔由你的測試套件應包含的三個層組成(從下到上):
一、Unit Tests
二、Service Tests
三、User Interface Tests
不幸的是,若是仔細觀察,測試金字塔的概念會有點短。有人認爲,麥克科恩的測試金字塔的命名或某些概念方面並不理想,我必須贊成。從現代的角度來看,測試金字塔彷佛過於簡單化,所以可能會產生誤導。
儘管如此,因爲它的簡單性,當創建本身的測試套件時,測試金字塔的本質是一個很好的經驗法則。你最好的選擇是記住Cohn最初的測試金字塔中的兩件事:
一、用不一樣的粒度編寫測試
二、更高的層次,更少的測試
堅持金字塔形狀,以提出一個健康,快速和可維護的測試套件:寫許多小而快的單元測試。 寫一些更粗粒度的測試和減小高級測試,從頭至尾測試你的應用程序。 注意,你最終不會獲得一個測試冰淇淋錐,這將是一個噩夢來維持,而且運行時間太長。
(譯者注:測試冰激凌錐的示意圖)
不要太拘泥於科恩測試金字塔中單個圖層的名稱。
事實上,它們可能會引發誤解:服務測試是一個難以理解的術語(科恩本人談論的觀察結果是許多開發人員徹底忽略了這一層)。在諸如react,angular,ember.js等單頁面應用程序框架的日子裏,UI測試顯然沒必要位於金字塔的最高層 - 在這些框架中你徹底能夠使用單元測試測試你的UI。
考慮到原始名稱的缺點,只要在代碼庫和團隊的討論中保持一致,就能夠爲測試圖層提供其餘名稱。
咱們會使用的工具和庫
JUnit: test runner
Mockito:mocking dependencies
Wiremock:用於剔除外部服務
Pact:用於編寫CDC測試
Selenium:用於編寫UI驅動的端到端測試
REST-assured:用於編寫REST API驅動的端到端測試
我已經寫了一個簡單的微服務,包括一個測試套件,其中包含測試金字塔中不一樣層次的測試。
示例應用程序顯示了典型的微服務的特徵。
它提供了一個REST接口,與數據庫交互並從第三方REST服務獲取信息。
它在Spring Boot中實現,即便你之前從未使用過Spring Boot,也應該能夠理解。
請務必查看Github上的代碼。
自述文件包含您在計算機上運行應用程序及其自動化測試所需的說明。
該應用程序的功能很簡單。 它提供了一個具備三個端點的REST接口:
GET / hello 返回「Hello Word」. 老是
GET / hello{lastname} 用提供的姓氏查找該人。
若是有這我的,則返回」Hello{Firstname}{Lastname}」.
GET /weather 返回當前的天氣情況 Hambur, Germany.
在高層次上,系統具備如下結構:
Figure 3: the high level structure of our microservice system
咱們的微服務提供了一個能夠經過HTTP調用的REST接口。對於某些端點,服務將從數據庫獲取信息。在其餘狀況下,該服務將經過HTTP調用外部天氣API來獲取並顯示當前天氣情況。
在內部,Spring服務有一個典型的Spring體系結構:
Figure 4: the internal structure of our microservice
控制器類提供REST端點並處理HTTP請求和響應
存儲庫類與數據庫接口並負責向持久存儲器寫入數據和從持久存儲器讀取數據
客戶端類與其餘API交互,在咱們的例子中,它經過darksky.net weather API的HTTPS獲取JSON
有經驗的Spring開發人員可能注意到這裏常用的圖層缺失:受Domain-Driven Design的啓發,不少開發人員構建了一個由服務類組成的服務層。我決定不在此應用程序中包含服務層。
其中一個緣由是咱們的應用程序很簡單,服務層原本就是沒必要要的間接層。 另一個是我認爲人們過分使用服務層。我常常遇到在服務類中捕獲整個業務邏輯的代碼庫。 Domain 模型僅僅成爲數據層,而不是行爲(Anemic Domain Model)。
對於每個不平凡的應用程序來講,這會浪費不少潛能來保持代碼的結構良好和可測試性,而且不能充分利用面向對象的功能。
咱們的存儲庫很是簡單,並提供簡單的CRUD功能。
爲了簡化代碼,我使用了Spring Data。Spring Data爲咱們提供了一個簡單而通用的CRUD存儲庫實現,咱們能夠使用它來代替咱們本身的實現。它還負責爲測試啓動內存數據庫,而不是像生產中那樣使用真正的PostgreSQL數據庫。看看代碼庫,讓本身熟悉內部結構。這對咱們的下一步將是有用的:測試應用程序!
測試套件的基礎將由單元測試組成。你的單元測試確保你的代碼庫的某個單元(你的受測主題)按預期工做。單元測試具備測試套件中全部測試的最小範圍。測試套件中的單元測試數量將遠遠超過任何其餘類型的測試。
Figure 5: A unit test typically replaces external collaborators with test doubles
若是你問三個不一樣的人在單元測試中的「單位」是什麼意思,你可能會收到四個不一樣的,微妙的答案。在必定程度上,這是一個你本身定義的問題,沒有標準答案。
若是你使用的是功能語言,一個單位極可能是一個單一的功能。你的單元測試將調用具備不一樣參數的函數,並確保它返回指望值。在面向對象的語言中,單元能夠從單一方法到整個類。
有些人認爲,被測主題的全部合做者(例如被測試的課程調用的其餘類)都應該用模擬或存根代替,以得到完美的隔離,避免反作用和複雜的測試設置。 其餘人則認爲只有緩慢或反作用較大的合做者(例如,訪問數據庫或進行網絡調用的類)應該被存根或模擬。
偶爾,人們會將這兩種測試標記爲孤獨的單元測試,測試將全部合做者和社交單元測試存儲在容許與真正合做者交談的測試中(Jay Fields的「有效地使用單元測試工做」創造了這些術語)。若是你有空閒時間,你能夠打開看一下,閱讀更多關於不一樣思想流派的優勢和缺點。
在一天結束時,決定是否進行單獨的或社交單元測試並不重要。重要的是編寫自動化測試。就我我的而言,我發現本身一直都在使用這兩種方法。若是使用真正的方法,合做者變得尷尬,我會慷慨地使用模擬和存根。
若是我以爲參與的合做者讓我對測試更有信心,那麼我只會將個人服務的最外面的部分存根。
Mocks和Stubs 是兩種不一樣類型的Test Doubles(不止這兩種)。許多人能夠互換地使用術語Mock和Stub。我認爲在腦海中精確保持其特定屬性是件好事。 你能夠使用test doubles 來替換你在生產中使用的對象,並使用來幫助你進行測試的實現。簡而言之,它意味着用一個假的版本替換了一件真實的東西(例如一個類,模塊或函數)。假的版本看起來和行爲像真實的東西(回答相同的方法調用),但你在單元測試開始時本身定義的預設迴應。使用test doubles並不特定於單元測試。更精細的test doubles可用於以受控方式模擬系統的整個部分。然而,在單元測試中,你極可能會遇到不少mock和stubs(取決於你是合做或獨立的開發人員),只是由於不少現代語言和庫讓設置變得簡單和溫馨。
不管你選擇何種技術,極可能語言標準庫或一些流行的第三方庫將提供優化的安裝模擬方法。 甚至從頭開始編寫你本身的模擬只是寫一個假的類/模塊/功能與真實的相同的簽名,並在測試中設置假的類。
單元測試運行速度很是快。在一臺情況良好的機器上,你能夠在幾分鐘內完成數千個單元測試。單獨測試小部分代碼庫,避免連接數據庫,文件系統或觸發HTTP查詢(經過使用這些部分的mock和stub)來保持測試的快速。
一旦掌握了編寫單元測試的竅門,你將會愈來愈流利地編寫。
剔除外部協做者,設置一些輸入數據,調用測試主題並檢查返回的值是否與預期相符。 看看測試驅動開發,讓單元測試指導你的開發; 若是正確應用,它能夠幫助你進入一個良好的流程,並提出良好的可維護設計,同時自動生成全面的全自動測試套件。儘管如此,這不是銀彈。還要繼續,嘗試一下,看看它是否適合你。
我真的須要測試這種私有方法嗎?
若是你發現本身真的須要測試私有方法,那麼你應該退後一步,問本身爲何。 我很肯定這是一個設計問題,而不是一個範圍問題。極可能你以爲須要測試一個私有方法,由於它很複雜,而且經過該類的公共接口來測試這個方法須要不少尷尬的設置。 每當我發現本身處於這種情況時,我一般會得出結論,我正在測試的這個類已經太複雜了。 它作得太多,違反了單一責任原則—SOLID原則中的S。 對我而言,解決方案一般是將原始類分紅兩個類。 一般只須要一兩分鐘的思考,就能夠找到一種把一個大班級分紅兩個小班並有我的責任的好辦法。 我將私有方法(我迫切想要測試)移動到新類中,並讓舊類調用新方法。 Voilà,我難以測試的私有方法如今是公開的,能夠很容易地測試。最重要的是,我堅持單一責任原則改進了個人代碼結構。
單元測試的好處在於,你能夠爲全部生產代碼類編寫單元測試,而無論它們的功能或內部結構屬於哪一個層。你能夠像測試存儲庫,域類或文件讀取器同樣單元測試控制器。 只需堅持one test class per production class,你就有了一個良好的開端。
單元測試類應該測試該類的公共接口。
私有方法沒法進行測試,由於你沒法從不一樣的測試類中調用它們。 受保護的或私有的包能夠從測試類訪問(考慮到測試類的包結構與生產類相同),但測試這些方法可能已經太過了。
編寫單元測試時有一條細線:它們應該確保測試全部不重要的代碼路徑(包括開心路徑和邊緣狀況)。同時它們不該該與你的實現過於緊密相關。
爲何會這樣?
太接近生產代碼的測試很快變得使人討厭。
只要重構生產代碼(快速回顧:重構意味着更改代碼的內部結構而不更改外部可見行爲),你的單元測試將會中斷。
這樣你就失去了單元測試的一大好處:充當代碼變動的安全網。你寧願厭倦那些每次重構都會失敗的愚蠢測試,這會致使更多的工做而不是幫助;並且其餘人會想誰寫這個愚蠢的測試?
你該作什麼呢?不要在你的單元測試中反映你的內部代碼結構,反而測試觀察行爲。將
若是我如數值 x 和 y, 結果會是 z 嗎?
代替爲
若是我輸入x和y,該方法會先調用類A,而後調用類B,而後返回類A的結果加上類B的結果?
私有方法一般應被視爲實施細節。
這就是爲何你甚至不該該有試探他們的衝動。
我常常聽到單元測試(或TDD)的反對者認爲編寫單元測試是毫無心義的工做,由於你必須測試全部的方法才能提升測試覆蓋率。
他們常常引用一個情景:過於熱心的團隊領導迫使他們爲getter和setter以及全部其餘種類繁瑣的代碼編寫單元測試,以便提供100%的測試覆蓋率。
這有太多的錯誤。
是的,你應該測試公共接口。但更重要的是,你不要測試不重要的代碼。 別擔憂,Kent Beck說不要緊。你不會從測試簡單的getter或setter或其餘不重要的實現(例如沒有任何條件邏輯)中得到任何東西。
節省時間,這是你能夠參加的又一次會議,萬歲!
全部測試的良好結構(這不只限於單元測試)是這樣的:
一、設置測試數據
二、在測試中調用你的方法
三、斷言預期的結果被返回
記住這種結構有一個很好的助記符:「排列,行動,斷言」(Arrange, Act, Assert)。 另外一個你能夠使用的靈感來自BDD。
它是「給定」(given),「當」(when),「而後」(then)三合一,給出反映了設置,當方法調用,而後斷言部分。
這種模式也能夠應用於其餘更高級別的測試。
在任何狀況下,他們都能確保你的測試保持簡單和一致的閱讀。除此以外,考慮到這種結構的測試每每更短,更具表現力。
專業的測試助手
不管在應用程序體系結構的哪一層,你均可覺得整個代碼庫編寫單元測試,這是一件美妙的事情。該示例顯示了對控制器的簡單單元測試。不幸的是,當談到Spring的控制器時,這種方法有一個缺點:Spring MVC的控制器大量使用註釋來聲明他們正在監聽哪些路徑,使用哪些HTTP動詞,他們從URL路徑解析哪些參數或者查詢參數等等。在單元測試中簡單地調用一個控制器的方法將不會測試全部這些關鍵的事情。幸運的是,Spring的貢獻者提出了一個很好的測試助手,能夠用它來編寫更好的控制器測試。確保檢查出MockMVC。它給你一個很好的DSL,你能夠使用它來對你的控制器發出假的請求,並檢查一切都沒問題。我在示例代碼庫中包含了一個示例。不少框架都提供了測試助手來使測試代碼庫的某些方面更加愉快。查看你選擇的框架的文檔,看看它是否爲你的自動化測試提供了有用的幫助。
如今咱們知道要測試什麼以及如何構建單元測試,終於能夠看到一個真實的例子。
咱們來看一個ExampleController類的簡化版本:
@RestController public class ExampleController { private final PersonRepository personRepo; @Autowired public ExampleController(final PersonRepository personRepo) { this.personRepo = personRepo; } @GetMapping("/hello/{lastName}") public String hello(@PathVariable final String lastName) { Optional<Person> foundPerson = personRepo.findByLastName(lastName); return foundPerson .map(person -> String.format("Hello %s %s!", person.getFirstName(), person.getLastName())) .orElse(String.format("Who is this '%s' youre talking about?", lastName)); } } hello(lastname)方法的單元測試以下所示: public class ExampleControllerTest { private ExampleController subject; @Mock private PersonRepository personRepo; @Before public void setUp() throws Exception { initMocks(this); subject = new ExampleController(personRepo); } @Test public void shouldReturnFullNameOfAPerson() throws Exception { Person peter = new Person("Peter", "Pan"); given(personRepo.findByLastName("Pan")) .willReturn(Optional.of(peter)); String greeting = subject.hello("Pan"); assertThat(greeting, is("Hello Peter Pan!")); } @Test public void shouldTellIfPersonIsUnknown() throws Exception { given(personRepo.findByLastName(anyString())) .willReturn(Optional.empty()); String greeting = subject.hello("Pan"); assertThat(greeting, is("Who is this 'Pan' you're talking about?")); } }
咱們正在使用JUnit編寫單元測試,這是Java事實上的標準測試框架。 咱們使用Mockito來替換真正的PersonRepository類和stub以供咱們測試。 這個stub容許咱們定義在這個測試中存根方法應該返回的罐頭響應。
Stub使咱們的測試更加簡單,可預測,而且使咱們可以輕鬆設置測試數據。
在安排(arrange),行動(act),斷言(assert)結構以後,咱們編寫了兩個單元測試 - 一個正面的案例和一個被搜查的人沒法找到的案例。
第一個正面的測試用例建立一個新的人物對象,並告訴模擬存儲庫在用「Pan」做爲lastName參數的值調用時返回該對象。
測試而後繼續調用應該測試的方法。 最後它斷言返回值等於預期的返回值。
第二個測試的工做原理相似,但在場景中,測試方法未找到給定參數的人。
全部非平凡的應用程序都將與其餘部分(數據庫,文件系統,對其餘應用程序的網絡調用)集成在一塊兒。
在編寫單元測試時,這些一般是你爲了提供更好的隔離和更快的測試而遺漏的部分。 儘管如此,應用程序仍會與其餘部分進行交互,並須要進行測試。集成測試能夠幫助你。他們會測試應用程序與應用程序以外的全部部分的集成。
對於自動化測試,這意味着不只須要運行應用程序,還須要運行正在與之集成的組件。 若是你正在測試與數據庫的集成,則須要在運行測試時運行數據庫。 爲了測試你能夠從磁盤讀取文件,須要將文件保存到磁盤並將其加載到集成測試中。
我以前提到「單元測試」是一個模糊的術語,對於「集成測試」來講更是如此。對於某些人來講,集成測試意味着要測試整個應用程序堆棧與系統中的其餘應用程序鏈接。我喜歡更狹窄地對待集成測試,而且一次測試一個集成點,經過將test doubles替換爲單獨的服務和數據庫。 結合合同測試和對test doubles運行合同測試以及真實實施,你能夠提出更快,更獨立而且一般更容易推理的集成測試。
狹窄的集成測試活在你服務的邊界。
從概念上講,它們始終是觸發一種致使與外部部分(文件系統,數據庫,單獨服務)集成的操做。 數據庫集成測試看起來像這樣:
Figure 6: A database integration test integrates your code with a real database
一、啓動一個數據庫
二、將你的應用程序鏈接到數據庫
三、在代碼中觸發一個將數據寫入數據庫的函數
四、經過讀取數據庫中的數據來檢查預期數據是否寫入了數據庫
另外一個例子,測試你的服務經過REST API與單獨的服務集成多是這樣的:
Figure 7: This kind of integration test checks that your application can communicate with a separate service correctly
一、開始你的申請
二、啓動單獨服務的一個實例(或者具備相同接口的test double)
三、在你的代碼中觸發一個從獨立服務的API中讀取的函數
四、檢查你的應用程序是否能夠正確解析響應
你的集成測試 - 好比單元測試 - 能夠是至關於白盒。
有些框架容許你啓動應用程序,同時仍然能夠模擬應用程序的其餘部分,以便檢查是否發生了正確的交互。編寫集成測試,用於序列化或反序列化數據的全部代碼段。這種狀況發生的頻率比你想象的要多。 想想:
調用你的服務的REST API
讀取和寫入數據庫
調用其餘應用程序的API
讀取和寫入隊列
圍繞這些邊界編寫集成測試可確保將數據寫入這些外部協做者並從中讀取數據能夠正常工做。
在編寫狹窄集成測試時,應該着眼於在本地運行外部依賴關係:啓動本地MySQL數據庫,對本地ext4文件系統進行測試。若是你要與單獨的服務集成,請在本地運行該服務的實例,或者構建並運行模仿真實服務行爲的假版本。若是沒法在本地運行第三方服務,則應選擇運行專用測試實例,並在運行集成測試時指向此測試實例。 避免在自動化測試中與實際生產系統集成。
將數以千計的測試請求發佈到生產系統是一種絕對讓人們生氣的方式,由於你的日誌混亂(最好的狀況下),甚至DoS的服務(最壞的狀況)。經過網絡集成服務是普遍集成測試的典型特徵,而且使測試變得更慢,一般更難以編寫。
關於測試金字塔,集成測試的級別高於單元測試。
集成文件系統和數據庫等慢速部件每每比運行單元測試要慢得多,而這些部件都被剔除了。畢竟,做爲測試的一部分,你必須考慮外部零件的旋轉,它們也可能比小而孤立的單元測試更難編寫。
不過,它們的優點在於讓您確信您的應用程序能夠正確處理所需的全部外部部件。 單元測試沒法幫助你。
PersonRepository是代碼庫中惟一的存儲庫類。 它依賴於Spring Data,並無實際的實現。
它只是擴展了CrudRepository接口並提供了一個單一的方法頭。 其他的是Spring魔術。
public interface PersonRepository extends CrudRepository<Person, String> { Optional<Person> findByLastName(String lastName); }
經過CrudRepository接口,Spring Boot經過findOne,findAll,save,update和delete方法提供了一個功能完備的CRUD存儲庫。
咱們的自定義方法定義(findByLastName())擴展了這個基本功能,併爲咱們提供了一種按姓氏提取PersonS的方法。 Spring Data分析了方法的返回類型及其方法名稱,並根據命名約定檢查方法名稱以找出它應該作什麼。
雖然Spring Data負責實現數據庫存儲庫,但我仍然編寫了一個數據庫集成測試。 你可能會爭辯說,這是測試框架和我應該避免的,由於它不是咱們正在測試的代碼。 不過,我相信至少有一個集成測試是相當重要的。首先它測試咱們的自定義findByLastName方法的行爲如預期。
其次,它證實咱們的存儲庫正確使用了Spring的接線並能夠鏈接到數據庫。
爲了讓你在機器上運行測試變得容易(無需安裝PostgreSQL數據庫),咱們的測試鏈接到內存中的H2數據庫。
我已經在build.gradle文件中將H2定義爲測試依賴項。test目錄中的application.properties沒有定義任何spring.datasource屬性。 這告訴Spring Data使用內存數據庫。由於它在類路徑上發現H2,因此它在運行咱們的測試時僅使用H2。
當使用int配置文件運行實際應用程序時(例如,經過將SPRING_PROFILES_ACTIVE = int設置爲環境變量),它將鏈接到application-int.properties中定義的PostgreSQL數據庫。
我知道,要了解和理解這些Spring細節是很是多的。爲了達到目的,你必須篩選大量的文檔。由此產生的代碼很容易理解,但若是你不瞭解Spring的細節,就很難理解。
除此以外,使用內存數據庫是危險的業務。
畢竟,咱們的集成測試針對的是不一樣於生產環境的不一樣類型的數據庫。 繼續並自行決定是否更喜歡使用Spring魔術方法和簡單的代碼,而不是更明確而更詳細的實現。
已經有足夠的解釋了,下面是一個簡單的集成測試,它將一個Person保存到數據庫中,並經過姓氏找到它:
@RunWith(SpringRunner.class) @DataJpaTest public class PersonRepositoryIntegrationTest { @Autowired private PersonRepository subject; @After public void tearDown() throws Exception { subject.deleteAll(); } @Test public void shouldSaveAndFetchPerson() throws Exception { Person peter = new Person("Peter", "Pan"); subject.save(peter); Optional<Person> maybePeter = subject.findByLastName("Pan"); assertThat(maybePeter, is(Optional.of(peter))); } }
你能夠看到,咱們的集成測試遵循與單元測試相同的arrange(排列),act(行爲)和assert(斷言)結構。告訴你,這是一個廣泛的概念!
咱們的微服務與darksky.net,一個天氣REST API交互。固然,咱們但願確保咱們的服務可以正確地發送請求並解析響應。
咱們但願在運行自動化測試時避免碰到真正的darksky服務器。
咱們免費計劃的配額限制只是緣由的一部分。 真正的緣由是解耦。 咱們在darksky.net的測試應該獨立於其餘人。
即便你的機器沒法訪問darksky服務器或darksky服務器因維護而停機。
在運行咱們的集成測試時,能夠經過運行咱們本身的虛假darksky服務器來避免碰到真正的darksky服務器。 這聽起來像是一項艱鉅的任務。
因爲像Wiremock這樣的工具,這很容易。 看這個:
@RunWith(SpringRunner.class) @SpringBootTest public class WeatherClientIntegrationTest { @Autowired private WeatherClient subject; @Rule public WireMockRule wireMockRule = new WireMockRule(8089); @Test public void shouldCallWeatherService() throws Exception { wireMockRule.stubFor(get(urlPathEqualTo("/some-test-api-key/53.5511,9.9937")) .willReturn(aResponse() .withBody(FileLoader.read("classpath:weatherApiResponse.json")) .withHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .withStatus(200))); Optional<WeatherResponse> weatherResponse = subject.fetchWeather(); Optional<WeatherResponse> expectedResponse = Optional.of(new WeatherResponse("Rain")); assertThat(weatherResponse, is(expectedResponse)); } }
要使用Wiremock,咱們在固定端口(8089)上實例化一個WireMockRule。 使用DSL能夠設置Wiremock服務器,定義它應該監聽的端點,並設置它應該響應的灌裝響應(canned responses)。
接下來咱們調用想要測試的方法,即調用第三方服務的方法,並檢查結果是否正確解析。
瞭解測試如何知道應該調用虛擬的Wiremock服務器而不是真正的darksky API很是重要。 祕密在咱們包含在src / test / resources中的application.properties文件中。這是運行測試時Spring加載的屬性文件。在這個文件中,咱們覆蓋了像API鍵和URLs這樣的配置,其值適合咱們的測試目的,例如調用虛擬的Wiremock服務器而不是真正的服務器:
weather.url = http://localhost:8089
請注意,這裏定義的端口必須與咱們在測試中實例化WireMockRule時所定義的端口相同。 經過在咱們的WeatherClient類的構造函數中注入URL,能夠將測試中的真實天氣API的URL替換爲假天氣:
@Autowired public WeatherClient(final RestTemplate restTemplate, @Value("${weather.url}") final String weatherServiceUrl, @Value("${weather.api_key}") final String weatherServiceApiKey) { this.restTemplate = restTemplate; this.weatherServiceUrl = weatherServiceUrl; this.weatherServiceApiKey = weatherServiceApiKey; }
這樣,咱們的WeatherClient從應用程序屬性中定義的weather.url屬性中讀取weatherUrl參數的值。
使用Wiremock等工具爲單獨服務編寫narrow integration tests很是簡單。 不幸的是,這種方法有一個缺點:咱們如何確保咱們設置的假服務器的行爲像真正的服務器?
在目前的實施中,單獨的服務可能會改變其API,咱們的測試仍然會經過。 如今咱們只是測試咱們的WeatherClient能夠解析假服務器發送的響應。 這是一個開始,但很是脆弱。
使用端到端測試並針對真實服務的測試實例運行測試而不是使用假服務能夠解決此問題,但會使咱們依賴於測試服務的可用性。
幸運的是,有一個更好的解決方案來解決這個困境:對虛假服務器和真實服務器運行合同測試可確保咱們在集成測試中使用的虛假測試是忠實的測試。 讓咱們看看接下來的工做。
更多的現代軟件開發組織已經找到了經過跨不一樣團隊開發系統來擴展其開發工做的方法。 個別團隊創建個別的,鬆散耦合的服務,而不用彼此踩腳趾,並將這些服務整合到一個大的,有凝聚力的系統中。
最近圍繞微服務的討論正是關注這一點。
將系統分割成許多小型服務經常意味着這些服務須要經過某些(但願定義明確的,有時意外增加的)接口相互通訊。
不一樣應用程序之間的接口能夠有不一樣的形狀和技術。 常見的是
REST和JSON經過HTTPS
使用相似gRPC的RPC
對於每一個接口,涉及兩方:提供者和消費者。 該提供商向消費者提供數據。 消費者處理從提供者處得到的數據。
在REST世界中,提供者使用全部必需的端點構建REST API;
消費者調用此REST API來獲取數據或觸發其餘服務中的更改。
在異步的,事件驅動的世界中,提供者(一般稱爲發佈者)將數據發佈到隊列中; 消費者(一般稱爲訂戶)訂閱這些隊列並讀取和處理數據。
Figure 8: Each interface has a providing(or publishing) and a consuming(or subscribing) party. The specification of an interface can be considered a contract.
因爲你常常在不一樣團隊之間傳播消費和提供服務,你會發現本身處於必須明確指定這些服務之間的接口(所謂的合同)的狀況。 傳統上,公司經過如下方式來解決這個問題:
編寫一份詳細的長期界面規範(合同)
按照定義的合同實施提供服務
將界面規範扔到圍欄上的消費團隊
等到他們實現他們消費接口的部分
運行一些大規模的手動系統測試,看看是否一切正常
更現代化的軟件開發團隊用更自動化的東西取代了第5步和第6步:自動契約測試確保消費者和提供者方面的實現仍然堅持已定義的合同。他們做爲一個很好的迴歸測試套件,並確保早期發現與合同的誤差。
在一個更敏捷的組織中,你應該採起更有效和浪費更少的路線。你在同一個組織內構建您的應用程序。直接與其餘服務的開發人員直接交談,而不是摒棄過於詳細的文檔,這不該該太難。畢竟他們是你的同事,而不是第三方供應商,你只能經過客戶支持或法律上的防彈合同進行交談。
消費者驅動合同測試(CDC測試)讓消費者推進合同的實施。使用CDC,接口的使用者編寫測試,從接口檢查接口所需的全部數據。而後消費團隊發佈這些測試,以便發佈團隊能夠輕鬆獲取並執行這些測試。支援團隊如今能夠經過運行CDC測試來開發他們的API。一旦全部測試經過,他們知道已經實施了消費團隊所需的一切。
Figure 9: 合同測試確保接口的提供者和全部消費者都堅持已定義的接口契約。 經過CDC測試,接口的消費者以自動化測試的形式發佈他們的需求;提供者不斷地獲取並執行這些測試
這種方法容許提供團隊只實施真正必要的事情(保持簡單,YAGNI(You ain’t gonna need it)等等)。
提供界面的團隊應持續(在他們的構建流水線中)獲取並運行這些CDC測試,以當即發現任何重大更改。
若是他們更改界面,他們的CDC測試將會失敗,從而阻止突發變化的發生。 只要測試保持綠色,團隊能夠進行他們喜歡的任何更改,而沒必要擔憂其餘團隊。 消費者驅動的合同方法會給你帶來一個看起來像這樣的過程:
消費團隊編寫符合全部消費者指望的自動化測試
他們爲提供團隊發佈測試
提供團隊持續運行CDC測試並保持綠色
若是你的組織採用微服務方法,進行CDC測試是創建自治團隊的重要一步。 CDC測試是促進團隊溝通的自動化方式。
他們確保團隊之間的界面隨時都在工做。
CDC測試失敗是一個很好的指標,你應該走到受影響的團隊,聊聊任何即將到來的API變化,並瞭解你想如何前進。
一個原始的CDC測試實現能夠像對API發起請求同樣簡單,並聲明響應包含你須要的全部東西。而後將這些測試打包爲可執行文件(.gem,.jar,.sh),並將其上傳到其餘團隊能夠獲取的地方(例如Artifactory等工件存儲庫)。
在過去的幾年中,CDC方法變得愈來愈流行,而且已經構建了幾種工具來使它們更容易編寫和交換。
Pact多是最近最突出的一個。
它具備爲消費者和提供商編寫測試的複雜方法,可爲你提供開箱即用的獨立服務存根,並容許您與其餘團隊交換CDC測試。
Pact已經被移植到不少平臺上,而且能夠與JVM語言,Ruby,.NET,JavaScript等一塊兒使用。
若是您想開始使用CDC而且不知道如何,Pact能夠是一個理智的選擇。
這些文檔可能會在第一時間壓倒一切。
保持耐心,並努力經過它。它有助於深刻了解疾病預防控制中心,從而使您在與其餘團隊合做時更容易倡導使用疾病預防控制中心。
消費者驅動的合同測試(CDC)能夠成爲一個真正的遊戲規則改變者,以創建自信的團隊,能夠快速而自信地行動。
幫你本身一個忙,閱讀這個概念並試一試。
一套可靠的CDC測試對於可以快速移動而不會破壞其餘服務並對其餘團隊形成很大的挫折,這個測試是無價的。
咱們的微服務使用天氣API。
所以,咱們有責任編寫一份消費者測試,以肯定咱們對微服務與天氣服務之間的合同(API)的指望。
首先,咱們在build.gradle中包含一個用於編寫契約消費者測試的庫:
testCompile('au.com.dius:pact-jvm-consumer-junit_2.11:3.5.5')
感謝這個庫,咱們能夠實現一個消費者測試並使用pact的模擬服務:
@RunWith(SpringRunner.class) @SpringBootTest public class WeatherClientConsumerTest { @Autowired private WeatherClient weatherClient; @Rule public PactProviderRuleMk2 weatherProvider = new PactProviderRuleMk2("weather_provider", "localhost", 8089, this); @Pact(consumer="test_consumer") public RequestResponsePact createPact(PactDslWithProvider builder) throws IOException { return builder .given("weather forecast data") .uponReceiving("a request for a weather request for Hamburg") .path("/some-test-api-key/53.5511,9.9937") .method("GET") .willRespondWith() .status(200) .body(FileLoader.read("classpath:weatherApiResponse.json"), ContentType.APPLICATION_JSON) .toPact(); } @Test @PactVerification("weather_provider") public void shouldFetchWeatherInformation() throws Exception { Optional<WeatherResponse> weatherResponse = weatherClient.fetchWeather(); assertThat(weatherResponse.isPresent(), is(true)); assertThat(weatherResponse.get().getSummary(), is("Rain")); } }
若是仔細觀察,你會看到WeatherClientConsumerTest與WeatherClientIntegrationTest很是類似。此次咱們不使用Wiremock做爲服務器stub,而是使用Pact。事實上,消費者測試與集成測試徹底同樣,咱們用一個stub替換真正的第三方服務器,定義指望的響應並檢查咱們的客戶端是否能夠正確解析響應。在這個意義上,WeatherClientConsumerTest自己就是一個小範圍的集成測試。與基於線鏈接的測試相比,這種測試的優勢是每次運行時都會生成一個pact文件(在target / pacts /&pact-name>.json中找到)。該協議文件以特殊的JSON格式描述了咱們對合同的指望。而後能夠使用此協議文件來驗證咱們的存根服務器的行爲與真實服務器的行爲相同。咱們能夠將協議文件交給提供界面的團隊。他們拿這個協議文件,並使用在那裏定義的指望寫一個提供者測試。這樣他們測試他們的API是否知足咱們全部的指望。
你會發現這是CDC消費者驅動部分的來源。
消費者經過描述他們的指望來推進接口的實現。提供者必須確保他們可以知足全部的指望,而且他們完成了。 沒有鍍金,沒有YAGNI和東西。
將協議文件提供給提供團隊能夠經過多種方式進行。一個簡單的方法是將它們放入版本控制並告訴提供者團隊老是獲取最新版本的協議文件。更多的進步是使用工件存儲庫,像亞馬遜S3或協議代理的服務。
開始簡單並根據須要增加。
在你的真實世界的應用程序中,你不須要二者,一個集成測試和一個客戶端類的消費者測試。示例代碼庫包含兩個向你展現如何使用任何一個。若是你想使用pact編寫CDC測試,我建議堅持使用後者。編寫測試的效果是同樣的。使用pact的好處是,您能夠自動得到一份pact文件,其中包含對其餘團隊能夠輕鬆實施其供應商測試的合同指望。固然,若是你能說服其餘團隊也使用pact,這是惟一有意義的。若是這不起做用,使用集成測試和Wiremock組合是一個體面的計劃b。
提供者測試必須由提供天氣API的人員執行。咱們正在使用dark sky.net提供的公共API。理論上,darksky team 將在他們的最後實施提供商測試,以檢查他們是否違反了他們的應用程序和咱們的服務之間的合同。
顯然,他們不關心咱們微不足道的示例應用程序,也不會爲咱們實施CDC測試。這是面向公衆的API和採用微服務的組織之間的巨大差別。面向公衆的API不可能考慮每一個用戶,不然他們將沒法前進。在你本身的組織中,能夠並且應該。你的應用極可能會爲少數幾個用戶提供服務,最多可能有幾十個用戶。爲了保持穩定的系統,會很好地編寫這些接口的提供者測試。
提供團隊獲取pact文件並針對其提供的服務運行該文件。爲此,他們實現了一個提供程序測試,讀取該文件,存儲一些測試數據,並根據他們的服務運行在pact文件中定義指望值。
Pact夥伴已經編寫了幾個庫來執行提供者測試。他們的主要GitHub repo爲你提供了一個很好的概覽,哪一個消費者和哪些提供程序庫可用。 選擇最適合你的技術堆棧的那個。
爲了簡單起見,咱們假設darksky API也是在Spring Boot中實現的。 在這種狀況下,他們能夠使用Spring的pact 提供者,它很好地鉤入Spring的MockMVC機制。 darksky.net團隊將執行的假設提供者測試可能以下所示:
@RunWith(RestPactRunner.class) @Provider("weather_provider") // same as the "provider_name" in our clientConsumerTest @PactFolder("target/pacts") // tells pact where to load the pact files from public class WeatherProviderTest { @InjectMocks private ForecastController forecastController = new ForecastController(); @Mock private ForecastService forecastService; @TestTarget public final MockMvcTarget target = new MockMvcTarget(); @Before public void before() { initMocks(this); target.setControllers(forecastController); } @State("weather forecast data") // same as the "given()" in our clientConsumerTest public void weatherForecastData() { when(forecastService.fetchForecastFor(any(String.class), any(String.class))) .thenReturn(weatherForecast("Rain")); } }
你會看到全部提供程序測試必須執行的操做是加載一個pact文件(例如,經過使用@PactFolder註釋來加載之前下載的協議文件),而後定義應如何提供預約義狀態的測試數據(例如,使用Mockito mocks)。沒有定製測試能夠被實施。這些都來自pact文件。Provider test 與消費者測試中聲明的provider name和狀態匹配的對應對象是很是重要的。
咱們已經看到如何測試咱們的服務和天氣提供商之間的合同。有了這個接口咱們的服務做爲消費者,天氣服務就像提供者同樣。進一步思考會看到,咱們的服務還充當其餘人的提供者:提供了一個REST API,它準備好供其餘人使用的端點。
正如剛剛瞭解的那樣,合同測試很是激烈,咱們固然也會爲這份合同寫一份合同測試。 幸運的是,正在使用消費者驅動契約(consumer-driven contracts),所以全部消費團隊都向咱們發送他們的Pacts,咱們能夠使用它們來爲咱們的REST API實現提供者測試。
首先,將Spring的Pact提供程序庫添加到項目中:
testCompile('au.com.dius:pact-jvm-provider-spring_2.12:3.5.5')
實現提供者測試的方式與以前描述的相同。爲了簡單起見,我將咱們的簡單消費者的pact文件輸入到咱們服務的存儲庫中。這使得目的更容易,在真實場景中,你可能會使用更復雜的機制來分發你的pact文件。
@RunWith(RestPactRunner.class) @Provider("person_provider")// same as in the "provider_name" part in our pact file @PactFolder("target/pacts") // tells pact where to load the pact files from public class ExampleProviderTest { @Mock private PersonRepository personRepository; @Mock private WeatherClient weatherClient; private ExampleController exampleController; @TestTarget public final MockMvcTarget target = new MockMvcTarget(); @Before public void before() { initMocks(this); exampleController = new ExampleController(personRepository, weatherClient); target.setControllers(exampleController); } @State("person data") // same as the "given()" part in our consumer test public void personData() { Person peterPan = new Person("Peter", "Pan"); when(personRepository.findByLastName("Pan")).thenReturn(Optional.of (peterPan)); } }
所示的ExampleProviderTest須要根據咱們提供的pact文件提供狀態,就是這樣。一旦運行提供程序測試,Pact就會拿起pact文件並針對咱們的服務發起HTTP請求,而後根據設置的狀態作出響應。
大多數應用程序都有某種用戶界面。
一般,咱們正在討論Web應用程序環境中的Web界面。人們常常忘記REST API或命令行界面與花哨的Web用戶界面同樣多的用戶界面。
UI tests測試應用程序的用戶界面是否正常工做。用戶輸入應該觸發正確的操做,數據應該呈現給用戶,UI狀態應該按預期改變。
UI測試和端到端測試有時(如Mike Cohn的案例)被認爲是一回事。
對我來講,這是兩個相互正交的概念。
是的,端到端測試你的應用程序一般意味着經過用戶界面來驅動您的測試。
然而,反過來倒是不正確的。
測試你的用戶界面不必定要以端到端的方式進行。
根據你使用的技術,測試用戶界面可能很是簡單,只需爲後端JavaScript代碼編寫一些單元測試並將其後端代碼刪除便可。
使用傳統的Web應用程序測試用戶界面能夠使用像Selenium這樣的工具來實現。若是你認爲REST API是你的用戶界面,應該經過圍繞API編寫適當的集成測試來得到所需的一切。
有了Web界面,可能須要在UI中測試多個方面:行爲,佈局,可用性,不多對公司設計的測試。
幸運的是,測試用戶界面的行爲很是簡單。你點擊這裏,在那裏輸入數據,並但願用戶界面的狀態相應地改變。現代的單頁面應用程序框架(react,vue.js,Angular等)一般帶有本身的工具和helpers,它們容許您以至關低級的(單元測試)方式完全測試這些交互。即便你使用vanilla javascript來實現本身的前端實現,你也能夠使用常規的測試工具,如Jasmine或Mocha。使用更傳統的服務器端渲染應用程序,基於Selenium的測試將是你的最佳選擇。
測試你的web應用程序的佈局是否保持無缺,有點困難。根據你的應用程序和你的用戶需求,可能須要確保代碼更改不會意外地破壞網站的佈局。
問題在於,計算機在檢查某些「看起來不錯」(多是一些聰明的機器學習算法可能在未來改變)方面是很是糟糕的。
若是你想要在構建管道中自動檢查Web應用程序的設計,有一些工具可供嘗試。這些工具中的大多數都利用Selenium以不一樣的瀏覽器和格式打開您的Web應用程序,截取屏幕截圖並將它們與之前拍攝的截圖進行比較。若是新舊截圖以意想不到的方式出現差別,該工具會通知您。
Galen是這些工具之一。
可是,若是您有特殊要求,即便推出本身的解決方案也不難。和我有合做的一些團隊已經創建了陣容和基於Java的表哥jimupup來實現相似的功能。兩種工具都採用了我以前描述的基於Selenium的方法。
一旦你想測試可用性和「看起來不錯」的因素,你就離開了自動化測試領域。這是你應該依賴探索性測試,可用性測試(這甚至能夠像走廊測試同樣簡單)的領域,並向用戶展現他們是否喜歡使用你的產品,而且能夠使用全部功能而不會感到沮喪或煩惱。
經過用戶界面測試已部署的應用程序是你能夠測試應用程序的最爲端到端的方式。 前面描述的webdriver驅動的UI測試是端到端測試的一個很好的例子。
當你須要肯定軟件是否正常工做時,端到端測試(也稱爲普遍堆棧測試)爲你提供最大的信心。經過Selenium和WebDriver協議,你能夠經過自動驅動(無頭)瀏覽器針對部署的服務,執行點擊操做,輸入數據並檢查用戶界面的狀態來自動執行測試。您能夠直接使用Selenium或使用基於它的工具,Nightwatch就是其中之一。
端到端測試帶來了各自的問題。
它們是出了名的碎片,每每因意外和不可預見的緣由而失敗。他們的失敗每每是一種誤解。你的用戶界面越複雜,測試越碎片。瀏覽器的怪癖,計時問題,動畫和意外的彈出對話框只是讓我花更多時間進行調試的一些緣由,而不是我想認可的。
在微服務世界中,誰負責編寫這些測試也是一個大問題。因爲它們跨越多個服務(整個系統),所以沒有一個團隊負責編寫端到端測試。
若是你有一個集中的質量保證團隊,他們看起來很合適。
然而,擁有一個集中的質量保證團隊是一個很大的反模式,在DevOps世界裏不該該有一席之地,你的團隊是真正意義上的跨職能團隊。誰應該擁有端到端的測試並不容易。也許你的組織有一個實踐社區或一個能夠照顧這些的高質量公會。找到正確的答案在很大程度上取決於你的組織。
此外,端到端測試須要大量維護,運行速度很是緩慢。
考慮到不止一兩個微服務的格局,你甚至沒法在本地運行端到端測試-由於這須要在本地啓動全部微服務。在你的開發機器上啓動了數百個應用程序,而不會炸燬你的RAM。
因爲維護成本高昂,應該儘可能減小端到端測試的數量。
考慮用戶在應用程序中使用的高價值交互。
嘗試提出定義產品核心價值的用戶旅程,並將這些用戶旅程中最重要的步驟轉化爲自動化的端到端測試。
若是你正在創建一個電子商務網站,你最有價值的客戶旅程多是一個用戶搜索產品,將其放入購物籃並結賬。僅此而已。
只要這個旅程仍然有效,不該該太麻煩。
也許你會發現一兩個更重要的用戶旅程,能夠將其轉化爲端到端測試。
除此以外,更多的事情均可能更痛苦。
請記住:你的測試金字塔中有不少較低的級別,已經測試了各類邊界案例並與系統的其餘部分進行了集成。 沒有必要在更高層次上重複這些測試。 高昂的維護工做量和大量的誤報會減慢你的速度,會讓你在測試中失去信心,宜早不宜遲。
對於端到端測試,Selenium和WebDriver協議是許多開發人員首選的工具。 使用Selenium,您能夠選擇一個你喜歡的瀏覽器,而後讓它自動調用你的網站,點擊界面這裏和那裏,輸入數據並檢查用戶界面中的變化。
Selenium須要一個能夠啓動並用於運行測試的瀏覽器。對於不一樣的瀏覽器,能夠使用多個所謂的「驅動程序」。
選擇一個(或多個)並將其添加到您的build.gradle。
不管你選擇哪一種瀏覽器,都須要確保團隊中的全部開發人員和你的CI服務器在本地安裝了正確版本的瀏覽器。保持同步可能會很是痛苦。對於Java,有一個很好的小型庫叫作webdrivermanager,它能夠自動下載並設置你想要使用的正確版本的瀏覽器。將這兩個依賴關係添加到你的build.gradle中,而後能夠繼續:
testCompile('org.seleniumhq.selenium:selenium-chrome-driver:2.53.1') testCompile('io.github.bonigarcia:webdrivermanager:1.7.2')
在測試套件中運行完整的瀏覽器可能會很麻煩。
特別是在使用持續交付時,運行管道的服務器可能沒法啓動包含用戶界面的瀏覽器(例如由於沒有X-Server可用)。
您能夠經過啓動像xvfb這樣的虛擬X-Server來解決此問題。
最近的方法是使用無頭瀏覽器(即沒有用戶界面的瀏覽器)來運行webdriver測試。 直到最近PhantomJS是領先的自動化的無頭瀏覽器。
自從Chromium和Firefox宣佈他們在瀏覽器中實現無頭模式後,PhantomJS忽然變得過期了。
畢竟,最好使用用戶實際使用的瀏覽器(好比Firefox和Chrome)來測試網站,而不是僅僅做爲開發人員方便你使用仿真瀏覽器。
無頭的Firefox和Chrome都是全新的,而且還沒有被普遍採用來執行webdriver測試。 咱們想保持簡單。
而不是擺弄一望無際的模式,讓咱們堅持使用Selenium和普通瀏覽器的經典方式吧。 一個簡單的端到端測試,使用Chrome瀏覽器,導航到咱們的服務,並檢查網站的內容以下所示:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloE2ESeleniumTest { private WebDriver driver; @LocalServerPort private int port; @BeforeClass public static void setUpClass() throws Exception { ChromeDriverManager.getInstance().setup(); } @Before public void setUp() throws Exception { driver = new ChromeDriver(); } @After public void tearDown() { driver.close(); } @Test public void helloPageHasTextHelloWorld() { driver.get(String.format("http://127.0.0.1:%s/hello", port)); assertThat(driver.findElement(By.tagName("body")).getText(), containsString("Hello World!")); } }
請注意,若是你在運行此測試的系統(本地計算機,你的CI服務器)上安裝了Chrome,該測試將僅在你的系統上運行。測試很簡單。它使用@SpringBootTest在一個隨機端口上運行整個Spring應用程序。而後,咱們實例化一個新的Chrome瀏覽器驅動程序,告訴它導航到咱們的微服務的/ hello端點,並檢查它是否在瀏覽器窗口中打印出「Hello World!」。這是很酷的東西!
在測試應用程序時避免使用圖形用戶界面是一個好主意,它能夠提供比較完整的端到端測試,同時仍涵蓋應用程序堆棧的大部份內容。當經過應用程序的Web界面進行測試特別困難時,這能夠派上用場。 也許你甚至沒有一個Web UI,而是提供一個REST API來代替(由於你有一個單獨的頁面應用程序在某個地方與該API交談,或者僅僅是由於你鄙視一切都很好)。 不管哪一種方式,一個Subcutaneous Test,只是在圖形用戶界面下進行測試,而且可讓你真正走遠,而不會對信心形成太大損失。 若是你像咱們的示例代碼那樣提供REST API,那就是正確的作法:
@RestController public class ExampleController { private final PersonRepository personRepository; // shortened for clarity @GetMapping("/hello/{lastName}") public String hello(@PathVariable final String lastName) { Optional<Person> foundPerson = personRepository.findByLastName(lastName); return foundPerson .map(person -> String.format("Hello %s %s!", person.getFirstName(), person.getLastName())) .orElse(String.format("Who is this '%s' you're talking about?", lastName)); } }
當我測試一個提供REST API的服務時,讓我再向您展現一個更方便的庫。
REST-assured
是一個爲你提供一個很好的DSL的庫,用於發送針對API的實際HTTP請求並評估你收到的響應。
首先要作的事情是:將依賴關係添加到build.gradle中。
testCompile('io.rest-assured:rest-assured:3.0.3')
藉助這個庫,咱們能夠爲咱們的REST API實施端到端測試:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloE2ERestTest { @Autowired private PersonRepository personRepository; @LocalServerPort private int port; @After public void tearDown() throws Exception { personRepository.deleteAll(); } @Test public void shouldReturnGreeting() throws Exception { Person peter = new Person("Peter", "Pan"); personRepository.save(peter); when() .get(String.format("http://localhost:%s/hello/Pan", port)) .then() .statusCode(is(200)) .body(containsString("Hello Peter Pan!")); } }
再次,咱們使用@SpringBootTest啓動整個Spring應用程序。
在這種狀況下,咱們@Autowire PersonRepository,以便咱們能夠輕鬆地將測試數據寫入咱們的數據庫。 當咱們如今要求REST API向咱們的朋友「潘先生」說「打招呼」時,咱們會獲得一個很好的問候。 很是好! 若是你甚至沒有運行網絡界面,那麼就能夠進行足夠多的端到端測試。
在測試金字塔中移動得越高,進入測試領域的可能性就越大,從用戶的角度來看,你構建的功能是否正常工做。
能夠將你的應用程序視爲黑盒子,並將測試中的焦點從下面中移除
當我輸入值x和y時,返回值應該是z
而是用
由於有一個登陸用戶(given there's a logged in user)
還有一篇文章「自行車」(and there's an article "bicycle")
當用戶導航到「自行車」文章的詳細頁面時(when the user navigates to the "bicycle" article's detail page)
並點擊「添加到籃子」按鈕(and clicks the "add to basket" button)
那麼文章「自行車」應該在他們的購物籃中(then the article "bicycle" should be in their shopping basket)
有時你會聽到這些測試的功能測試或驗收測試的條款。
有時人們會告訴你功能和驗收測試是不一樣的東西。這些術語是混淆的。甚至有時候人們會無休止地討論措辭和定義。一般這種討論會引發至關大的混亂。這才重要:在某一時刻,你應該確保從用戶的角度測試軟件是否正常工做,而不只僅是從技術角度。 你認爲這些測試真的不是那麼重要。
然而,進行這些測試是有必要的。
選擇一個,堅持下去,而後編寫這些測試。
這也是人們談論BDD和使您可以以BDD方式實施測試的工具的時刻。
BDD或BDD風格的編寫測試方式多是一個不錯的竅門,可將你的思想從實施細節轉移到用戶需求。 繼續嘗試吧。
你甚至不須要像Cucumber那樣採用全面的BDD工具(儘管你能夠)。
有些斷言庫(好比chai.js容許你用should樣式的關鍵字來編寫斷言,這樣可讓你的測試可以讀取更多相似於BDD的內容。即便你不使用提供這種表示法的庫,聰明且分工合理的代碼 將容許你編寫以用戶行爲爲中心的測試。一些輔助方法/函數能夠爲你帶來很長的路要走:
# a sample acceptance test in Python def test_add_to_basket(): # given user = a_user_with_empty_basket() user.login() bicycle = article(name="bicycle", price=100) # when article_page.add_to_.basket(bicycle) # then assert user.basket.contains(bicycle)
驗收測試能夠有不一樣的粒度級別。
大多數時候他們將會至關高級並經過用戶界面測試您的服務。然而,理解在技術上不須要在測試金字塔的最高級別編寫驗收測試是很好的。
若是你的應用程序設計和手頭的場景容許您在較低的級別上編寫驗收測試,那就去作吧。 進行低級測試比進行高級測試要好。 驗收測試的概念 - 證實功能爲用戶正確地工做 - 徹底與測試金字塔正交。
即便是最用功的自動化測試也不完美。 有時候你會錯過自動化測試中的某些邊緣狀況。 有時經過編寫單元測試來檢測特定的錯誤幾乎是不可能的。 某些質量問題在您的自動化測試中甚至不明顯(考慮設計或可用性)。 儘管你對測試自動化有着最好的意圖,但某些類型的手動測試仍然是一個好主意。
Figure 12: Use exploratory testing to spot all quality issues that your build pipeline didn’t spot
在測試組合中包含探索性測試。 這是一種手動測試方法,強調測試人員的自由和創造力,以便在運行中的系統中發現質量問題。
只需按期安排一些時間,捲起袖子並嘗試破壞應用程序。
使用破壞性的思惟方式,想出辦法在應用程序中引起問題和錯誤。 記錄您之後找到的全部內容。
注意錯誤,設計問題,響應時間緩慢,丟失或誤導性的錯誤信息以及其餘一切會讓你做爲軟件用戶煩惱的事情。
好消息是,你能夠使用自動化測試你大部分發現。爲你發現的錯誤編寫自動化測試,確保未來不會出現該錯誤的任何回退。此外,它還能夠幫助在錯誤修復期間縮小問題的根源。
在探索性測試過程當中,你會發現經過你的構建管道未被注意到的問題。不要感到沮喪。 這對您的構建管道的成熟度有很好的反饋。
與任何反饋同樣,請務必採起行動:考慮你未來能夠採起什麼措施來避免這些問題。也許你錯過了一些自動化測試。
也許在此次迭代中對自動化測試嗤之以鼻,而且須要在未來進行更完全的測試。 也許有一種閃亮的新工具或方法能夠用來避免未來出現這些問題。
請務必採起行動,以便管道和整個軟件交付將走得更遠變得更加成熟。
談論不一樣的測試分類老是很困難。
當我談論單元測試時,個人意思可能與你的理解稍有不一樣。
若是是集成測試,狀況更糟。
對於某些人來講,集成測試是一項很是普遍的活動,能夠測試整個系統的許多不一樣部分。 對我而言,這是一個至關狹隘的東西,一次只測試一個外部部件的集成。 一些人稱他們爲集成測試,一些人稱他們爲組件測試,一些人更喜歡術語服務測試。
甚至其餘人也會爭辯說,全部這三個術語都是徹底不一樣的東西。 沒有對錯。
軟件開發社區根本沒有設法圍繞測試定義明確的術語。
不要太拘泥於模棱兩可的話。
若是您稱之爲端到端或普遍的堆棧測試或功能測試,則可有可無。
若是你的集成測試對你來講意味着與其餘公司的人不一樣,那就不要緊了。 是的,若是咱們的專業可以按照一些明肯定義的條件解決而且所有堅持下去,那將會很是好。 不幸的是,這尚未發生。
並且,因爲在編寫測試時有不少細微差異,反而比一堆離散的存儲桶更像一個頻譜,這使得一致的命名更加困難。
重要的是,你應該找到適合你和你的團隊的條款。清楚你想寫的不一樣類型的測試。 就團隊中的命名達成一致,並就每種類型的測試範圍達成共識。 若是你在團隊內部(或者甚至在你的組織內)得到這種一致性,那麼你應該關心的就是這些。 當Simon Stewart描述他們在Google使用的方法時,Simon Stewart總結得很是好。
並且我認爲這徹底代表,讓名字和命名慣例過於沉悶是不值得的麻煩。
若是你使用的是持續集成或持續交付,那麼將擁有一個部署管道,每次對軟件進行更改時都會運行自動化測試。
一般這個管道分紅幾個階段,逐漸讓你更加確信軟件已準備好部署到生產環境。 聽到全部這些不一樣類型的測試,你可能想知道如何將它們放置在部署管道中。 要回答這個問題,應該考慮持續交付(其實是極限編程和敏捷軟件開發的核心價值之一)的基本價值之一:快速反饋。
一個好的構建管道告訴你,儘量快地搞砸。你不想等一個小時才能發現你的最新代碼更改破壞了一些簡單的單元測試。若是你的管道須要很長時間才能給反饋,那麼你極可能已經回家了。經過快速運行的測試放在流水線的早期階段,能夠在幾秒鐘內得到這些信息,也可能須要幾分鐘。相反,在較晚的階段,較長時間的運行測試(一般是較寬範圍的測試)放在不會推遲快速運行測試的反饋。你會發現定義部署管道的階段不是由測試類型驅動的,而是由速度和範圍決定的。考慮到這一點它能夠是一個很是合理的決定,把一些真正的狹義範圍的和快速運行的集成測試在同一個舞臺上你的單元測試 - 僅僅是由於他們給你更快的反饋,而不是由於你想畫沿着你的測試的正式類型。
如今你知道你應該寫出不一樣類型的測試,但還有一個能夠避免的錯誤:在金字塔的不一樣層次上重複測試。
雖然你的直覺可能會說不須要太多的測試。我向你保證,須要。
測試套件中的每一項測試都須要額外的時間,並非免費的。
編寫和維護測試須要時間。 閱讀和理解其餘人的測試須要時間。
固然,運行測試須要時間。
與生產代碼同樣,應該儘可能簡化並避免重複。
在實施你的測試金字塔的背景下,你應該記住兩條經驗法則:
一、若是較高級別的測試發現錯誤,而且沒有較低級別的測試失敗,則須要編寫較低級別的測試
二、儘量將測試推到測試金字塔的盡頭。
第一條規則很重要,由於較低級別的測試可讓你更好地縮小錯誤並以獨立方式複製錯誤。 當調試手頭的問題時,它們會運行得更快,而且不會臃腫。 它們將成爲將來良好的迴歸測試。
第二條規則對於快速保持測試套件很是重要。
若是你已經在較低級別的測試中自信地測試了全部條件,則不須要在測試套件中保留更高級別的測試。 它只是沒有增長更多的信心。
多餘的測試會在平常工做中變得煩人。
你的測試套件會變慢,當你改變代碼的行爲時你須要改變動多的測試。
讓咱們以不一樣的方式來表述:若是更高級別的測試讓你更加確信應用程序正常工做,那麼你應該擁有它。
爲Controller類編寫單元測試有助於測試Controller自己的邏輯。
不過,這並不能告訴這個Controller提供的REST端點是否實際響應HTTP請求。 因此,移動測試金字塔並添加一個測試來檢查確切的 - 但沒有更多。
你不要測試低級測試已經在高級測試中覆蓋的全部條件邏輯和邊界狀況。
確保較高級別的測試側重於較低級別測試沒法覆蓋的部分。
當涉及到不提供任何價值的測試時,我很是嚴格。
我刪除了較低級別的高級測試(由於它們不提供額外的值)。
若是可能的話,我用較低級別的測試替換更高級別的測試。
有時候這很難,特別是若是你知道提出一個測試是艱苦的工做。
謹防沉沒成本謬誤並敲擊刪除鍵。
沒有理由浪費更多寶貴的時間在再也不提供價值的測試上。
就像通常編寫代碼同樣,寫出良好和乾淨的測試代碼須要很是細心。
在繼續以前提出可維護的測試代碼以及在自動化測試套件中破解,如下是一些更多提示:
一、測試代碼與生產代碼同樣重要。 給它一樣的關注。 「這只是測試代碼」不是證實草率代碼合理的理由
二、每一個測試測試一個條件。 這能夠幫助你保持測試簡短而且容易推理
三、「安排,採起行動,斷言」或「當時,那麼」是很好的助記符,可讓你的測試保持良好的結構
四、可讀性很重要。 不要試圖過分DRY(Don’t repeat yourself)。 複製是能夠的,若是它提升可讀性的話。 嘗試在DRY和DAMP代碼之間找到平衡點
五、若是有疑問,請使用三條規則來決定什麼時候重構。 重用以前使用
就這樣!我知道這是一個漫長而艱難的閱讀,解釋爲何以及如何測試你的軟件。 好消息是,這些信息是持久有用的,而且不管你正在構建什麼樣樣的軟件。 不管您是從事微服務領域,物聯網設備,移動應用程序仍是Web應用程序,這些文章的教訓均可以應用到全部這些領域。
我但願在這篇文章中有一些有用的東西。
如今繼續查看示例代碼,並將這裏介紹的一些概念加入到您的測試組合中。 有一個堅實的測試組合須要一些努力。
它將會在更長的時間內獲得回報,而且會讓你的開發者更加安寧,相信我。
Thanks to Clare Sudbery, Chris Ford, Martha Rohte, Andrew Jones-Weiss David Swallow, Aiko Klostermann, Bastian Stein, Sebastian Roidl and Birgitta Böckeler for providing feedback and suggestions to early drafts of this article. Thanks to Martin Fowler for his advice, insights and support.