基於testcontainers的現代化集成測試進階之路


大型的軟件工程項目除了大量的產品級代碼外必不可少的還有大量的自動化測試。自動化測試包含從前端到後端甚至到產品線上不一樣模塊和環境的各類類型的測試。一個比較經典的關於自動化測試分佈的理論就是測試金字塔,是說在一個正常的項目中合理的測試數量應該是單元測試 > 組件測試 > 集成測試 > 端到端測試(系統測試)> 人工驗證測試。這個理論大致上是合理的,由於從測試代碼的複雜度和執行時間看單元測試 < 組件測試 < 集成測試 < 端到端測試(系統測試)< 人工驗證測試,因此咱們理所固然應該分配更多的時間和精力到容易理解和執行快速的測試中去,好比單元測試。固然關於這些測試分類和界定的見解衆說紛紜,好比組件測試和集成測試,有時甚至是端到端測試,都一律被稱爲集成測試,由於它們在不一樣的系統層面試圖去測試兩個模塊或者系統間的集成情況。
前端




最經典的集成測試的例子應該是後端系統應用層和數據層之間的集成測試了吧。數據層能夠是傳統的數據庫,也能夠是Kafka Stream這樣的新寵。一般這種集成測試有幾種思路:
git

  1. 部署到staging環境中,而後在測試中發送請求到系統A,那個請求會包含對數據層系統B的讀寫操做。這個其實算是跳過集成測試到了端到端測試。但這種思路弊端不少,測試代碼複雜度高,路徑覆蓋率低,從寫出bug到檢測到bug的週期也很長,不是理想的解決方案。
  2. 在測試中使用In-memory Embedded Database(一般是實際數據庫系統B的純內存化實現版本,主要用於這種測試環境裏),這樣就能細化到測試系統A裏面模塊X對數據庫B的某個寫操做,並且能夠在本地編寫、運行、調試,和上面的解決方案比已經有了很大的改進。可是這個解決方案仍是有幾個弊端:
    1. 不少In-memory Embedded Database只提供一個特定版本的實現,好比MongoDB 3.2,但若是你的實際數據庫版本是4.0,那麼不少新的數據庫功能在測試里根本覆蓋不了。
    2. 有些In-memory Embedded Database甚至沒有實現100%的接口兼容,或者不同的實現方式,好比關係型數據庫的transaction實現。這意爲着就算你的測試過了,線上的代碼仍是可能會出錯。這是常見的生產環境和測試環境不一致性問題。

受益於Docker的普及化,testcontainers提供了另一種更爲友好的集成測試解決方案。簡單地講就是在測試環境中動態建立須要的依賴服務的容器,好比動態建立一個Mongo 3.6的容器、建立一個RabbitMQ 最新發布版的容器,而後在測試中配置測試環境讓測試應用使用建立好的容器暴露的可調用地址,測試結束後把使用過的容器銷燬防止依賴服務狀態遷移致使其餘的測試莫名地掛掉。
github


這種解決方案有如下幾個優勢:
面試

  • 每一個Test Group都能像寫單元測試那樣細粒度地寫集成測試,保證每一個集成單元的高測試覆蓋率
  • Test Group間是作到依賴隔離的,也就是說它們不共享任何一個Docker容器;假如兩個Test Group都要用到Mongo 4.0,會建立兩個容器供它們單獨使用
  • 保證了生產環境和測試環境的一致性,代碼部署到線上時不會遇到由於依賴服務接口不兼容而致使的bug
  • Test Group能夠並行化運行,減小總體測試運行時間。相比較有些in-memory 的依賴服務實現沒有實現很好的資源隔離,好比端口,一旦並行化運行就會出現端口衝突。
  • 得益於Docker,全部測試均可以在本地環境和CI/CD環境中運行,測試代碼調試和編寫就如同寫單元測試

固然,它也有幾個劣勢:
redis

  • 測試運行時間長:由於每一個Test Group須要動態建立和銷燬Docker容器,這兩個步驟不少時候佔用了大部分測試運行時間。固然客觀地講,這個等待時間仍是秒級別的,因此仍是能接受的。若是你再並行運行測試,整體運行時間仍是可控的。
  • 測試編寫、調試體驗由於上面一點而受到影響
  • 資源佔用率高:大部分的build agent都是一個虛擬機,甚至是一個docker進程,再加上還要給每一個Test Group分配資源跑它們的依賴服務,整個build agent的CPU、內存使用率都會增長很多。在繁忙的時候甚至出現性能退化問題。解決方法就是scale up/out build agent。


從編程語言支持度來講,目前testcotainers的github org上提供了Java, Scala, Go, Rust, NodeJs, Python, C#的類庫。從成熟度來講確定是Java的類庫最爲成熟,已被很多開源項目使用。其餘語言的類庫能夠想象不可避免會有些坑須要踩。

舉一個官網的例子來講明如何使用testcontainers類庫:docker

public class RedisBackedCacheIntTest {

    private RedisBackedCache underTest;

    // rule {
    @Rule
    public GenericContainer redis = new GenericContainer<>("redis:5.0.3-alpine")
                                            .withExposedPorts(6379);
    // }


    @Before
    public void setUp() {
        String address = redis.getContainerIpAddress();
        Integer port = redis.getFirstMappedPort();

        // Now we have an address and port for Redis, no matter where it is running
        underTest = new RedisBackedCache(address, port);
    }

    @Test
    public void testSimplePutAndGet() {
        underTest.put("test", "example");

        String retrieved = underTest.get("test");
        assertEquals("example", retrieved);
    }
}複製代碼

上面的JUnit測試中動態建立了一個redis:5.0.3-alphine容器,在setUp方法裏獲取該容器的公開地址和接口從而建立咱們要測試的RedisBackedCache實例,而後在測試裏輕輕鬆地調用該實例的方法、驗證結果。

testcontainers Java 提供了幾個現成的使用頻率較高的容器的類封裝,好比大部分數據庫(MySQL, Postgres, Cassandra, Neo4j), UI測試的Webdriver,ElasticSearch,Kafka, Nginx等等。若是你沒找到現成的封裝,你老是能夠調用更底層的`GenericContainer`。它也支持主流的Java測試框架,JUnit4, JUnit 5, TestNG,Spock。總的來講對於寫Java的同窗這個類庫使用起來仍是很是爽的!

看了這麼多,你是否是也心動了呢?! 那就快快行動起來吧,常言道:會哭的孩子有奶吃,會寫測試的開發有飯吃!
數據庫


2019.4.14
編程


本文原載於: 【技術博客】基於testcontainers的現代化集成測試進階之路
後端

----------------------------------------------------------------------------------------bash

藝與術公衆號(various__artists): 分享關於編程、軟件工程、系統架構、前沿技術的藝與術的思考。


相關文章
相關標籤/搜索