前言
從2017年初開始,到現在差不多半年多的時間,我這邊投入了一部分精力用於項目的自動化測試建設工作。目前來看收益還是比較明顯的,在這個過程中也加深了對自動化測試的理解,這邊就總結下自己對自動化測試的認識。
首先我想說下在開展自動化前后,我的工作狀況的對比:
-
去年2016年Q3、Q4,基本上天天處於996甚至997的狀態。大部分時間都花在了功能測試保障、回歸測試和上線驗證。因為項目在線上有多達7,8個不同的集群,每一次版本上線為了保險起見,開發會一個集群一個集群小心翼翼地上線,所以基本上天天都處於上線的狀態。每次上線,手工測試時間順利的話在半小時左右,如果遇到問題跟開發聯調定位,會達到數個小時。除了時間上開銷很大之外,每次上線帶來的精神上的壓力其實更嚴重,非常害怕哪個用戶半夜跳出來反饋說調度有Bug。
-
在開展了自動化測試之后,現在我給“日常版本迭代測試”只預估了25%的工作量。任意集群上線,我只要負責點一下Jenkins的“開始構建”按鈕,即可完成驗收。如果有出現用例失敗,會自動發送郵件告知開發。
顯然,目前的工作狀態要好很多,是因為項目工作量減少了嗎?顯然不是。其實今年以來整個項目組在研發的投入要比去年更多,工作量只會比以前更重,還要兼顧多個私有化部署的客戶的驗收和日常測試保障工作,工作量肯定是只增不減的。之所以能有更多的時間空余出來做其它更多維度的事情,這一切都得益於“自動化測試”的幫助,它極大地解放了我的手工測試時間,同時更加提升了上線的信心。
1. 需求和目標
在我開展自動化測試之前,其實該項目以前的測試人員也已經寫了很多的接口測試用例,但是大多數用例處於“半癱瘓”狀態,在CI上無人維護(聽說起初是有人維護的,但是后來用例多了,維護的人每次花很長時間去定位問題,結果卻發現大部分的問題都是環境問題導致,花了半天時間定位卻沒什么收益,久而久之便不想去維護)。看起來,自動化似乎並沒有什么收益,反而維護用例會造成額外的工作負擔。
我覺得,其實自動化測試跟其它任何一種測試類型(比如異常測試、穩定性測試、性能測試等)都是類似的,它也是一種測試類型而已。在開展測試之前,我們首先必須要明確自動化測試的需求是什么,要解決什么樣的問題。
1.1 讓“自動化”代替“手動”
在我看來,初期的自動化測試,我的目標很明確,我就是要讓“自動化”代替“手動”,讓自動化真正地跑起來,凡是“自動化”跑過的內容,我絕不再去手工重復執行一遍。這樣至少我有一個很明確的收益:每完成一條自動化用例,我減少了一條手工用例的執行時間。
必須要提醒的是,讓“自動化”完全替代“手動”,其實對自動化用例的穩定性、容錯都有一定的要求。你要花一定時間去思考用例執行過程中的異常場景,是否足以充分替代手工測試。因此,我在增加用例的時候都會非常謹慎,確保用例集是穩定100%通過的前提下才會增加新的用例。
對於正常情況下(排除環境、開發代碼的問題)有時100%通過,有時90%通過的自動化用例集,我覺得它的作用和參考價值為0。正常的用例集就應該是100%通過的。
1.2 讓“回歸”自動化
上節說了讓“自動化”替代“手動”,每完成一條自動化用例都是有明顯收益的。那如何讓收益最大化呢,當然是讓每次回歸或上線驗證“不得不”執行的用例優先自動化。如果完成了回歸用例集的全部自動化,那我就可以用它來替代我的日常回歸,和上線回歸工作,極大地釋放我的手工驗證時間。
這里必須要指出的是,我跟的項目其實是一個對系統穩定性的要求要高於新功能的引入的一個后台項目,所以它的核心功能是比較固定的,其實大多數后台項目也是類似的,核心功能聚合、對系統的穩定性要求高。這就需要保障系統的核心功能完善。所以我們可以先將“核心功能”的驗證完成自動化。
1.3 不要讓環境成為瓶頸
前面說了,舊的用例集在維護的過程中給測試人員增加了很多額外的負擔,到最后發現很多都是環境的問題。當時的情形就是專門搭建了另一套測試環境專門用於自動化測試,而大數據的后台環境搭建和維護非常的復雜,如果同時維護多套環境,難免會在一些組件升級的過程中出現遺漏,導致環境不同步。因此,我們的自動化測試用例前期完全可以直接在功能測試環境執行,因為功能測試環境肯定是會一直隨着版本的迭代向前不斷更新的。
2. 技術選型
在明確了目標后,要開始技術選型。常見的自動化測試類型,包括
- 接口自動化
- UI自動化
- 基於shell交互命令執行的自動化
此外,不屬於測試范疇,但是也可以實現自動化、釋放手工時間的還有
- 數據准備自動化
- 環境編譯、部署、打包自動化
- 穩定性測試/性能測試結果指標獲取、校驗自動化
- 機器資源監控、報警自動化
- 其它所有手工重復執行的操作
在開始自動化之前,首先要分析項目的架構和狀況。對於一個后端的服務,它如果是純粹以接口的形式提供給其它組件去調用,那可以采取“接口自動化”;對於一個Web產品,如果前后端都在測試的保障范圍,而且前端頁面相對比較穩定,可以考慮采用“UI自動化”(此時接口自動化其實已經不足以保障產品的端到端功能);對於更后端的組件,如果想測試組件自身的基礎核心功能,可以采用“基於shell交互命令執行的自動化”,通過自動化腳本的方式封裝shell命令的調用。
此外,有些人可能還會執着於編程語言的選擇,是用Java還是Python還是Shell,或者其它語言等等。這個我覺得其實沒有定論,可以根據自己對語言的偏好和熟練程度,但是必須要考慮團隊成員的普遍技術棧,因為后期可能其他人來接手這個項目時需要代替你去維護測試工程。通常來說,測試框架的選擇(不管是接口自動化、UI自動化)推薦使用Java的TestNG框架;對於簡單的基於命令行執行的自動化腳本的編寫推薦使用Shell(Shell非常地強大);對於稍復雜的一些自動化的腳本的編寫,推薦使用Python,在Python中可以非常方便地封裝Shell命令,同時Python區別於Shell的一個特性就是它支持面向對象的封裝,可以將一些對象封裝在特定的類中,增加程序的可讀性和健壯性。
這里再插一段題外話:有些人可能會疑惑,現在其實有很多接口測試平台,測試人員可以直接在平台上完成接口測試,在選型時怎么抉擇?——這里我不評價哪種方式更好,只想說下自己的看法:我覺得兩種其實各有各的好處:
- 編寫代碼的方式:
優點:提升自己的編碼能力,問題定位能力,具備更高的靈活性和可操作性。 缺點:結果展示不直觀,不易於協作。其他人維護代碼困難,難以推動開發執行。
- 接口平台的方式:
優點:簡便,上手容易,可以在項目組間很好的協作和維護,測試記錄和結果一目了然。 缺點:離開了平台,可能又要回歸手動。
對於測試人員而言,如果有精力和時間的話,我建議是兩種都要掌握,甚至是自己去開發接口測試平台的能力。
3. 自動化實施過程
目前我跟的項目里已經實現自動化的內容包括:基於接口的場景回歸自動化測試、編譯部署過程自動化、Jacoco覆蓋率統計並接入CR平台(代碼變更分析平台)的自動化、對外/上線打包發布的自動化、穩定性測試結果校驗的自動化。
下面着重介紹下項目的接口自動化框架的搭建和設計過程。
3.1 准備工作
老生常談,開始自動化前,我仍然想再次強調一定要明確自己的需求是什么。在我的項目里,我的需求主要有以下幾點:
- 同一份代碼可以在多個集群執行
- 各個集群的測試數據相互獨立,不會互相影響
- 可以方便地與數據庫進行交互
- 當用例執行出錯時,有詳細的日志幫助定位
- 較好的可維護性和集群擴展性。
3.2 框架搭建
3.2.1 環境搭建
環境搭建時,主要用了以下工具:
- Git:管理代碼工程
- TestNG:作為測試框架
- Maven:管理依賴包
- Log4j:管理日志
- Hibernate:實現數據庫交互
- HttpClient:實現請求發送
之所以沒有用MyBatis,覺得相對來說,MyBatis是一個半ORM的框架,它需要自己額外維護一份sql映射文件,而Hibernate是全ORM的,可以省去這一步。關於它倆的比較,大家可以參考下知乎的一篇文章:MyBatis和Hibernate的對比。對於JDBC的方式,當然它也可以訪問數據庫,只不過相對來說,使用ORM框架可以更貼近面向對象的編程方式。
3.2.2 不同集群配置管理
在實現過程中,因為不同的集群會有不同的配置,比如webserver host、登陸后台webserver的用戶名/密碼、公共賬號信息、數據庫信息等等。為了讓一份代碼可以在不同集群去共用,就必須把這些配置信息從代碼中剝離出來。可以用配置文件的形式來統一管理集群的配置信息,如圖所示:

每個文件代表一個集群的配置。在代碼中可以通過java.util.Properties類讀取配置文件的方式載入各項配置信息:
/**
* 根據指定的配置文件名,初始化配置
* @param configFile
* @throws IOException
*/
public PropertiesUtil(String configFile) throws IOException{
this.configFile =DEFAUL_CONFIG_FILE_DIRECTORY + configFile;
InputStream fis = new FileInputStream(this.configFile);
props = new Properties();
props.load(fis);
//關閉資源
fis.close();
}
/**
* 根據key值讀取配置的值
* @param key key值
* @return key 鍵對應的值
* @throws IOException
*/
public String readValue(String key){
return props.getProperty(key);
}
/**
* 讀取properties的全部信息
* @throws FileNotFoundException 配置文件沒有找到
* @throws IOException 關閉資源文件,或者加載配置文件錯誤
*
*/
public Map<String,String> readAllProperties(){
//保存所有的鍵值
Map<String,String> map=new HashMap<String,String>();
Enumeration<?> en = props.propertyNames();
while (en.hasMoreElements()) {
String key = (String) en.nextElement();
String property = props.getProperty(key);
map.put(key, property);
}
return map;
}
到這里,解決了配置讀取的問題,還需要解決代碼運行時如何讓它自己去選擇正確的集群配置文件的問題。我是將選擇配置文件的邏輯全部封裝到了一個工廠類BaseConfigFactory.java中,在實際測試使用時,我只需要通過工廠類的靜態方法BaseConfigFactory.getInstance()去獲取想要的配置信息,而不需要關心它到底是如何去選擇正確的配置文件的。工廠類的實現可以參考:
public class BaseConfigFactory {
private static final String testEnv= System.getenv("TEST_ENV") == null ? "null" : System.getenv("TEST_ENV");
private static Logger logger = Logger.getLogger(BaseConfigFactory.class);
private static BaseConfig baseConfig;
private static HashMap<String, String> clusterConfigMap;
public static synchronized BaseConfig getInstance(){
if (null == baseConfig){
PropertyConfigurator.configure("log4j.properties");
initMap();
setupConfig();
}
return baseConfig;
}
public static void initMap(){
clusterConfigMap = new HashMap<>();
clusterConfigMap.put("TEST-BJ", "test-bj.properties");
clusterConfigMap.put("ONLINE-BJ", "online-bj.properties");
clusterConfigMap.put("ONLINE-XS", "online-xs.properties");
clusterConfigMap.put("ONLINE-LT", "online-lt.properties");
clusterConfigMap.put("ONLINE-BEIJING", "online-beijing.properties");
clusterConfigMap.put("ONLINE-HD", "online-hd.properties");
clusterConfigMap.put("null", "test-local.properties");
}
public static void setupConfig(){
logger.info("TEST ENV: " + testEnv);
String propertyFile = clusterConfigMap.get(testEnv);
logger.info("Using '" + propertyFile + "' as property file.");
baseConfig = new BaseConfig(propertyFile);
}
}
即,將所有的集群的配置放入到一個Map中,然后通過讀取環境變量TEST_ENV的值來選取具體的集群配置文件clusterConfigMap.get(testEnv)。
3.2.3 log4j日志管理
良好的日志輸出是幫助定位問題的關鍵環節,尤其是定位服務器上執行時出現的問題。這邊貼一個log4j的配置:
### set log levels ###
log4j.rootLogger = debug, stdout, D, E
### 輸出到控制台 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Thread-%t] %m%n
### 輸出到日志文件 ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = logs/console.log
log4j.appender.D.Append = true
##輸出Debug級別以上的日志##
log4j.appender.D.Threshold = INFO
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Thread-%t] %m%n
### 保存異常信息到單獨文件 ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
##異常日志文件名##
log4j.appender.E.File = logs/error.log
log4j.appender.E.Append = true
##只輸出ERROR級別以上的日志##
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Thread-%t] %m%n
##Hibernate日志級別設置
log4j.logger.org.hibernate.ps.PreparedStatementCache=WARN
log4j.logger.org.hibernate=ERROR
# Changing the log level to DEBUG will result in Hibernate generated
# SQL to be logged.
log4j.logger.org.hibernate.SQL=ERROR
# Changing the log level to DEBUG will result in the PreparedStatement
# bound variable values to be logged.
log4j.logger.org.hibernate.type=ERROR
該配置將INFO級別和ERROR級別的日志分別定位輸出到不同的文件,且日志文件會按照日期進行自動歸檔,輸出的格式包含了日志的日期、級別、類信息、線程信息、日志內容等。
一般情況下,對於接口測試,當接口測試用例失敗時,我們要打印的日志包括:請求的url、參數、方法、實際響應、期望響應等等。
3.3 分層設計、解耦
首先看一下項目的工程目錄:

可以看到,項目中包含了多個package,各個package的作用已經在圖片中標示了。以前好多測試人員的習慣是將api代碼的調用、測試方法的編寫、data Provider的編寫、測試數據的構造全部寫在一個類文件中,這樣做其實會有幾個問題:
- 可讀性差
- 代碼復用性低
- 維護性差
- 難以調試
- 耦合帶來的其它各類問題
此外,如果不同集群的測試數據不同,會有大量的if判斷,結果是災難性的。
下面以一個用例為例,展示代碼的結構:
測試api:
public class ScheduleApi extends BaseAzkabanApi{
...
...
/**
* 使用默認公共賬號、email、失敗策略、sla報警郵箱新增正常調度。
* @param projectName
* @param flow
* @param projectId
* @param scheduleTime
* @param scheduleDate
* @param period
* @return
*/
public ResponseCode addNormSched(String projectName, String flow, String projectId, String scheduleTime, String scheduleDate,String period){
return scheduleFlow(projectName, flow, projectId, scheduleTime, scheduleDate, defaultProxyUser, defaultProxyEmail, period, defaultSlaEmail);
}
...
...
}
測試代碼test:
@Test(singleThreaded=true)
public class ScheduleTest{
...
...
/**
* 新增正常調度
* @param projectName
* @param flow
*/
@Test(priority=1, dataProvider="addNormSched", dataProviderClass=ScheduleDataProvider.class, testName="1410356")
public void addNormSched(String projectName, String flow, String expectedStatus, String hasScheduleId, String message){
ResponseCode rc= scheduleApi.addNormSched(projectName, flow);
Assert.assertEquals(rc.getStatus(), expectedStatus, message+rc.getDebugInfo("返回結果中的狀態status對應值"));
Assert.assertEquals(rc.hasProperty("scheduleId"), Boolean.parseBoolean(hasScheduleId), message+rc.getDebugInfo("返回結果中是否包含scheduleId"));
}
...
...
}
測試用例dataProvider:
public class ScheduleDataProvider {
@DataProvider(name = "addNormSched", parallel=true)
public static Object [][] addNormSched(){
return new Object[][]{
ScheduleTestData.validNormSchedule,
ScheduleTestData.notExistedProject,
ScheduleTestData.notExistedFlow
};
}
...
...
}
測試數據testdata:
public class ScheduleTestData extends BaseTestData{
...
...
//Testdata for addNormSched
public static Object[] validNormSchedule={VALID_PROJECT_NAME, VALID_NORMAL_SCHEDULE_FLOW, "success", "true", "設置有效的正常調度"};
public static Object[] notExistedProject={NOT_EXIST_PROJECT_NAME, VALID_NORMAL_SCHEDULE_FLOW, "error", "false", "不存在的project"};
public static Object[] notExistedFlow={VALID_PROJECT_NAME, NOT_EXIST_FLOW_NAME, "error", "fasle", "不存在的flow"};
...
...
}
可以看到,用例的測試代碼test類是非常簡潔的,只要調用api類封裝的接口,然后進行assert判斷即可。
關於測試數據,將dataprovider與testdata進行分離,也是為了后續可能會靈活地調整下架用例,只需要去除dataprovider類中的用例行即可,而testdata中的數據仍然可以留着復用。
另外,前面提到了不同集群測試數據的管理。再介紹下我這邊的實現方式:
- 不同測試類使用的公共數據,存放於BaseTestData基類中,讓其它testdata類繼承於基類
- 不同集群可以共用的數據,盡量共用,以常量的方式存儲於testdata類中
- 不同集群無法共用的數據,統一存放於特定的json文件管理
關於json文件管理數據,其實跟配置文件的管理類似,如下圖所示:

History.json:
{
"validTotalFetch":{
"key":"",
"beginTime":"2017-06-30%2015:30",
"endTime":"2017-06-30%2015:50",
"expectedTotal":"7"
},
"validImmediatelyFetch":{
"key":"instant_execute_job",
"beginTime":"2017-06-30%2013:30",
"endTime":"2017-06-30%2013:40",
"expectedTotal":"1"
},
"validScheduledFetch":{
"key":"online_schedule_job",
"beginTime":"2017-06-30%2014:30",
"endTime":"2017-06-30%2014:40",
"expectedTotal":"2"
}
}
3.4 改進與提升
在自動化的實施過程中,還遇到了一些問題可能對其它項目也會有一定的借鑒意義。這邊羅列下幾個我覺得比較有意思的問題。
3.4.1 webserver高可用的支持
我們的后台webserver是支持高可用的,所以每次運維上線后webserver的host可能會發生變化,以及在服務運行過程中也可能會發生webserver切換。如果每次去手動調整自動化用例的配置信息,是一件非常麻煩的事情。
解決的方式就是在配置文件中,將主從webserver的host都填寫進去,在測試過程中,如果發生請求失敗,則允許切換一次host。
3.4.2 用例並發執行
由於我們的一部分用例是異步的場景用例,需要執行一個數據開發的任務,然后等待其執行完成。這些用例的執行比較費時,如果順序執行的話會消耗非常多的時間。因此可以通過並發執行測試的方式,解決用例耗時的問題。關於TestNG的並發可以參考這篇文章:《簡單聊聊TestNG中的並發》
3.4.3 單例模式解決session問題和host重復切換問題
-
問題1: Azkaban的每個接口,都需要一個必傳參數seesion。這個session可以通過/login接口獲取。如果每個接口在執行的時候都去調用一次/login接口重新獲取session,就會顯得很冗余,也可能導致舊的session失效。
-
問題2: 上述提到的對webserver高可用的支持,當多條用例並行執行時如果同時去切換host,可能會造成host切換回原來的不可用host。
對於問題1,可以將session作為單例的方式進行存儲。
對於問題2,可以借鑒單例模式的“雙重檢查”思想,對切換host的代碼進行部分同步,在防止host重復切換的同時,不會降低httpclient請求的並發性。關於單例模式的應用可以參考這篇KS文章:《“單例模式”學習及其在優化接口自動化測試代碼中的實踐》
3.4.4 “變”與“不變”
其實這也是所有設計模式的基本思想,即區分自動化測試中的“可變因素”和“不變因素”。我覺得ycwdaaaa大神(飛哥)有兩句話是非常棒的:
- 封裝"一切"可能的可控的變化因素
- 為了穩定使盡"一切"手段
4. 結合研發過程的應用
上面介紹了一些自動化的實施過程,這邊再介紹下實施之后在項目研發過程中的應用。
目前在項目中,主要有以下幾方面的應用。
(1)提測后的自動化回歸驗收
下圖是項目的一條持續集成pipeline。在開發提測后,我會自動化地完成以下事情:
- 編譯代碼
- 將服務部署到各個機器,並完成Jacocod Agent的部署
- 執行靜態代碼檢查
- 執行接口測試
- 完成覆蓋率統計
- 將覆蓋率統計數據接入到CR平台
當自動化用例全部執行通過時,說明系統的核心功能回歸沒有問題,然后開始版本的細粒度功能的測試。

(2)Bug修復后的回歸驗收
在測試過程中,開發肯定會經常修復bug重新提交代碼,每次有代碼重新提交時,我都可以一鍵完成部署、測試、覆蓋率統計。
(3)上線后的回歸驗證
目前,項目的上線驗證已經完全由自動化驗證來替代。
(4)作為開發冒煙的一部分(未完成)
目前已經跟開發達成一致,開發非常歡迎將自動化用例接入到開發環境,用於他們每次變更時的環境正確性驗證,可以盡早幫助他們發現研發過程中出現的問題。並且在提測前,只有100%通過自動化測試才可以進行提測。
(5)線上監控
目前各個線上集群,都部署了自動化測試用例,這部分用例會每隔4小時執行一次。用於確保線上環境的穩定性。從效果上來看,線上監控的成效是非常明顯的,提前發現了很多集群的延遲問題,環境問題等,讓開發可以及時地收到報警,了解線上集群的情況。
(6)關於持續集成
可能有人會發現,上述的執行過程其實不是真正意義上的持續集成,真正意義上的持續集成應該是:每次開發提交代碼,自動觸發構建。
必須要承認的是,確實是如此。但是不管怎么樣,我覺得可以先從優化測試工作量的角度慢慢去推開整個流程,其實業界目前也並沒有確切的定論說只有持續集成才是最佳的實踐。相反,一味地持續集成可能會增加我們的維護成本。只要我們能切實提升自己的工作效率,達到目的就可以了。
5. 成效
當自動化做的比較完善后,你真的會發現:生活原來可以變得如此簡單美好。
自動編譯部署:測試過程中開發修復bug提交代碼是非常頻繁的,每次的手動編譯部署可能都會耗費十幾分鍾,並且測試人員的關注點還不能離開。
自動打包發布:從這個版本開始,所有集群的上線都會統一使用QA發布的包。這樣減少了以前每次上線時,開發運維人員要花費大量的時間逐一去拉取各個集群的代碼再進行編譯、部署。一鍵的打包發布,可以在上線前就提前准備好各個集群的上線包,開發只需調用部署腳本去獲取這些包,然后替換就可以完成上線。此外,自動打包發布的方式極大減少了運維上線時漏操作的風險。
自動化回歸測試:以前一次回歸測試,需要QA持續地投入超過30分鍾。現在通過一鍵執行,程序會自動地執行,時間控制在5分鍾以內。且QA可以將注意力放到其它事情上。
自動化完成穩定性測試結果的校驗:從前執行完穩定性測試,需要對着數據庫的一大片數據進行人肉地校驗。會耗費一個下午大半天的時間,甚至還是有遺漏。通過腳本自動校驗,1分鍾內就可以出結果報告。
這里再提一下UI自動化。很多人會對UI自動化有看法,覺得投入產出比不明顯、維護成本高。我認為UI自動化跟接口自動化其實沒有區別,都是功能回歸的一種形式而已,選擇哪種自動化的類型應該取決於項目的實際情況需要。另外,UI自動化的維護成本目前一個季度做下來看,真的沒有比接口自動化要高,關鍵還在於自動化的設計上是不是做的易於維護。
6. 展望
可以看到以上的自動化都是基於環境的穩定可用為前提的。之所以沒有獨立分配一套環境用於自動化測試,也是因為環境維護的成本較高。但是,基於測試人員的增加,測試類型的豐富(異常、性能),在一套環境上執行所有測試顯然會出現相互影響的問題。因此,如果能將測試環境搭建docker化,通過維護docker鏡像的方式,自動化地使用docker鏡像快捷地部署一套新的完整測試環境可以極大地提高我們的測試效率。
最后的最后,發自真心地希望圈中的前輩大神能給予我一些參考意見和指導,指出我的不足之處。謝謝!
