[持續交付實踐] 基於 Junit 的接口自動化測試框架實現
前言
這半個月基本都在出差以及各種公司業務上的事情,難得有空閑整理一些測試技術上的事情。周末有些空閑抓緊碼一篇填坑,持續交付/持續集成這一系列文章不僅僅是想在壇子裏和同行者做些分享,對個人的一種自我思考和鞭策。總體來說我覺得這個論壇目前還比較清爽,希望在人氣快速提升的同時能保持初心,堅持做一個單純技術分享交流的平臺。
分層的自動化測試
5~10年前,我們接觸的自動化測試更關註的是UI層的自動化測試,Mercury的WinRunner/QTP是那個時代商業性自動化測試產品的典型代表,在那個時代大家單純想的都是能用一個自動化操作的工具替代人力的點擊,商業化或是私有化框架大行其道。
而分層的自動化測試倡導產品的不同階段(層次)都需要自動化測試。在《google軟件測試之道》中,在google 70%的投入為單元測試(小型測試),20%為接口/集成測試(中型測試),10%為UI層的自動化測試(大型測試),也就是大家熟悉的金字塔模型,越往上自動化實現難度越大,投入產生的收益也越低(需要強調的是,UI層的自動化測試作為最接近用戶操作的測試,仍然有其存在的意義和場景)。
接口測試的意義
接口測試是驗證兩個或多個模塊應用之間的交互(通常是采用接口的方式),測試的重點是要檢查數據的交換,傳遞和控制管理過程,還包括處理的次數。
接口測試的核心戰略在於:以保證系統的正確和穩定為核心,以持續集成為手段,提高測試效率,提升用戶體驗,降低產品研發成本。
接口測試要為代碼的編寫保駕護航,增強開發人員和測試人員的自信,讓隱含的 BUG提前暴露出來,要讓開發人員在第一時間修復 BUG,要讓業務測試人員在測試的時候更加順手,最大限度得減少底層 BUG 的出現數量,要讓產品研發的流程更加敏捷,要縮短產品的研發周期,最後在產品上線以後,要讓用戶用得更加順暢,要讓用戶感覺產品服務零缺陷。
不同於單元測試,接口測試本質上還是一種黑盒的測試,所以非常適合專職測試工程師去參與和覆蓋。
接口測試框架選型
1.目前接口測試框架的選型,最常見的方法是采用jmeter,soapUI,postman,robotframework等UI化的接口測試框架來做。
好處是業務測試人員可以不用或很少寫測試代碼,入門門檻低,前幾年有很多公司都曾經開發過類似的測試框架,有前端有後端,專職的測試開發人員維護,業務測試人員只需要知道怎麽操作而不需要參與具體coding。
這種方法看起來非常高大上,但實際的問題是執行過程中主要的工作變成了測試框架的維護,非常依賴專職測試開發人員的設計和開發能力,每增加一種新的接口協議(比如dubbo、hessian或者內部自定義的協議)就需要在框架上增加支持;更致命的是一旦核心測試開發人員出現流動,就很容易造成整個接口測試體系的崩塌;另外對業務測試人員的技能成長也並不公平,個人已面試過太多只會使用某大公司XXX測試框架卻完全不了解具體實現方式的工程師。
《google軟件測試之道》中早已有過預言,保密和私有化的基礎測試設施並不能獲得想象中的好處,這種方式意味著昂貴和遲緩,即使在公司內部的不同項目之間也很難做到復用。未來的測試基礎設施必然是建立在共享代碼和開源框架的基礎上,測試開發人員需要更多的利用開源項目並為之貢獻。
最近重讀了一次這本四五年前幾乎改變軟件測試行業的書籍,發現裏面的預言都是如此準確,當然也可以認為國內整個行業都正參照google的方式在進行演變。
2.使用junit、testng等java接口框架,直接編寫測試代碼去測試,同時對一些重復性的工作抽象建立基礎庫或方法。
有點類似於單元測試,這種方法擴展性好實現靈活,作為程序員可以用代碼實現靈活的場景組織和功能,只要稍微二次開發一下,但需要測試工程師有一定的編碼基礎。
這種方式在前幾年實施的難度還是比較大,因為在市場上要找到懂java代碼的測試工程師都寥寥無幾,但在對測試工程師開發能力要求越來越多的今天實施難度已沒有想象中困難,java/python等語言的編碼能力也已成為我們團隊招聘時的基本要求。
另外提下,這裏使用java而不用其他語言的原因,主要是團隊的技術儲備java是強項,擁有豐富的開源測試庫,而且一般互聯網公司的產品基本都是采用java框架進行開發,和開發團隊技術棧保持一致非常有必要性。
我們封裝的接口測試框架
有很多公司做了各種不同的接口框架,都是基於自己公司的業務基礎設計開發。我們基於自己的業務特點(為避免廣告嫌疑,盡量我不提及具體公司的信息),也封裝了自己的接口測試框架gtest-framework,在開發人員的單元測試中也正逐漸使用。
gtest-framework要做的事情:
1.前置數據準備和自動清理。
2.常見接口協議的實現和封裝。
3.依賴註入配置方式的支持。
4.如文件、圖片、xml、字符等各類通用處理方法的集成。
5.斷言方式的擴展等。
接口測試關鍵實踐
1.數據準備
接口測試的數據準備,一般是指數據庫的數據準備,有時候還包括文件和緩存的數據準備。具體實現可以從下面幾個方面去考慮
(1)硬編碼的方式準備測試數據,在寫測試代碼的時候,使用到什麽數據就插入什麽數據。為了避免數據重復,很多人會習慣於使用隨機字符或隨機數(這種方法可能造成測試用例不穩定,盡量避免)。
(2)可以直接通過調用其他API的方式準備測試數據,這種情況在測試最上層服務的時候比較有用,比如測試購買商品,就需要準備要購買的商品數據,購買商品的用戶數據,這個時候,可以直接調用生成商品的api和生成用戶的api直接生成測試數據。此方法實現簡單,但前提是需要具備相應的api並且此api功能正確。
(3)使用excel或xml準備測試數據,這種準備測試數據的方式,主要針對對象數據的準備,比如可以將一條商品數據對應excel中的一條數據,因為一般開發都會使用pojo映射,而在準備測試數據的時候,這些pojo對象屬性的設置往往是重復和大工作量的,用excel或XML方式準備,則可以減少在代碼當中重復去準備這些數據。
一般我們使用的是2/3兩種方式,其中3這種方式主要利用dbunit、spring-test、unitils等測試框架的特性經二次開發增加自定義註解,很輕松的導入excel或xml格式的文件並在測試完成後對數據進行自動回滾。
/**
* @ClassName: TestJdbcDataSet
* @Description: 采用自定義TestDataSet註解方式準備測試數據,推薦。
* @author Cay.Jiang
* @date 2017年7月10日 上午9:10:29
*
*/
public class TestJdbcDataSet extends BaseCase{
Map<String, Object> args = new HashMap<String, Object>();
@Test
@TestDataSet(locations={"/tmp/domaininfo.xls"},dsNames={"mysqlDataSource"})
public void test01_mysql(){
args.put("selfdomain", "baidupc2");
List<Map<String, Object>> result=JdbcUtil.queryData(mysqlJdbcTemplate, "domaininfo", args);
System.out.println(result);
assertEquals("百度合作PC端接入2",result.get(0).get("remark"));
}
}
上面代碼中的/tmp/domaininfo.xls參見:domaininfo.xls ,其中Excel格式以Sheet名為表名,第一行定義了字段名稱,其余行為對應的數據。
多數據集:
@TestDataDataSet(locations={"Data1.xls","Data2.xls"},dsNames={"dsNameA","dsNameB"}),Data1.xls的數據會插入dsNameA所指的數據庫中,Data2.xls的數據會插入dsNameB所指的數據庫中
2.斷言
常見的斷言方式有JUnit自帶的Assert和Hamcrest。JUnit自帶的斷言方法功能十分有限只能滿足最基本的需求。Hamcrest相對來講功能豐富一些,但是該庫已經多年不更新。而且Hamcrest和JUnit自帶的斷言方法一樣,有個致命的缺點,就是當一個case中有多個斷言時,如果其中一個斷言失敗,那麽在它之後的斷言都不會執行。這裏向大家推薦一款新的斷言神器AssertJ。
AseertJ: 號稱流式斷言。什麽是流式,常見的斷言器一條斷言語句只能對實際值斷言一個校驗點,而AseertJ支持一條斷言語句對實際值同時斷言多個校驗點,這樣使得斷言的語句更加簡潔適合閱讀。AseertJ還支持一次性執行所有斷言,然後收集所有失敗的斷言一起反饋。當然除此之外AseertJ還有很多其他特性,可以參考官方文檔慢慢挖掘。下面將舉例說明一下AseertJ的優勢:
public class TestCase extends BaseCase{
UserProfileBO user = new UserProfileBO();
@Before
public void init(){
user.setAddress("杭州");
user.setMobile("1386800000");
user.setUserName("測試賬號");
}
/*
* JUnit 內置的斷言
*
* 1、其中一個斷言失敗後,後面所有斷言將不會執行。
* 2、支持的斷言方法較少
*
*/
@Test
public void testAssertJUnit(){
Assert.assertEquals("地址",user.getAddress(),"寧波");
Assert.assertEquals("手機",user.getMobile(),"13868000000");
Assert.assertEquals("手機",user.getUserName(),"測試");
Assert.assertNotNull(user.getMobile());
Assert.assertTrue(user.getMobile().startsWith("138"));
Assert.assertTrue(user.getMobile().length() == 11);
}
/*
* Hamcres 斷言
*
* 1、其中一個斷言失敗後,後面所有斷言將不會執行。
* 2、支持的斷言方法豐富,但是已經多年不更新。
*
*/
@Test
public void testHamcrestMatchers() {
MatcherAssert.assertThat(user.getAddress(), equalTo("寧波"));
MatcherAssert.assertThat(user.getMobile(), equalTo("13868000000"));
MatcherAssert.assertThat(user.getUserName(), equalTo("測試"));
MatcherAssert.assertThat(user.getMobile(), allOf(is(nullValue()),startsWith("136")));
}
/*
* AssertJ 斷言
*
* 1、支持所有斷言執行後,失敗斷言統一反饋。
* 2、支持的斷言方法豐富。
* 3、支持流式斷言,方便閱讀。
*
*/
@Test
public void testAssertJ(){
//斷言集合,執行所有斷言後,失敗斷言統一反饋。
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(user.getAddress().equals("寧波"));
softly.assertThat(user.getMobile().equals("13868000000"));
softly.assertThat(user.getUserName().equals("測試"));
});
//流式斷言
Assertions.assertThat(user.getMobile())
.isNotNull()
.startsWith("136")
.hasSize(