本文為霍格沃茲測試學院優秀學員課程學習筆記。
上篇文章提到了資料驅動可以在幾個方面進行:
測試資料的資料驅動 測試步驟的資料驅動 定位符 行為流 斷言的資料驅動下面將詳細解說如何進行資料驅動。
5. 資料驅動
5.1 測試資料的資料驅動
5.1.1 Junit5的 引數化@ParameterizedTest+@ValueSource引數化說到測試資料的資料驅動,就必然離不開測試框架的引數化,畢竟測試資料是傳給用例的,用例是由框架來管理的,這裡以目前最推薦使用的Junit5框架為例,介紹引數化的使用
在Junit5中,提供了@ParameterizedTest註解來實現方法的引數化設定,另外@ValueSource註解用來存放資料,寫法如下:
@ParameterizedTest@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate));}
@ParameterizedTest+@CsvSource引數化
Junit5還提供了@CsvSource註解來實現csv格式的引數傳遞,寫法如下:
@ParameterizedTest@CsvSource({ "滴滴,滴滴出行", "alibaba,阿里巴巴", "sougou,搜狗"})public void searchStocks(String searchInfo,String exceptName) { String name = searchpage.inputSearchInfo(searchInfo).getAll().get(0); assertThat(name,equalTo(exceptName));}
@ParameterizedTest+@CsvFileSourc資料驅動最終,Junit5提供了@CsvFileSourc註解來實現csv資料格式的資料驅動,可以傳遞csv檔案路徑來讀取資料,寫法如下:
csv資料檔案:pddxiaomipdd
用例實現:
@ParameterizedTest@CsvFileSource(resources = "/data/SearchTest.csv")void choose(String keyword){ArrayList<String> arrayList = searchPage.inputSearchInfo(keyword).addSelected();}
@ParameterizedTest+@MethodSource引數化先來看Junit5提供的另一個註解——@MethodSource,此註解提供的方法是我們做測試資料驅動的核心,它可以讓方法接收指定方法的返回值作為引數化的入參,用法是在註解的括號中填入資料來源的方法名,具體用法如下:對於簡單的資料結構,可以使用CSV,上面也說過,較為複雜的資料結構,推薦使用yaml,接下來看如何用yaml檔案完成測試資料驅動。
@ParameterizedTest@MethodSource("stringProvider")void testWithExplicitLocalMethodSource(String argument) { assertNotNull(argument);}static Stream<String> stringProvider() { return Stream.of("apple", "banana");}
@ParameterizedTest+@MethodSource引數化 + jackson yaml資料驅動有了@MethodSource的引數化支援,我們就可以在方法中利用jackson庫對yaml檔案進行資料讀取,從而完成資料驅動了
現有如下yaml資料檔案,我需要取出testdata中的測試資料 username: 888 password: 666 testdata: 滴滴: 滴滴出行 alibaba: 阿里巴巴 sougou: 搜狗
建立Config類:
import java.util.HashMap;public class Config { public String username; public String password; public HashMap<String,String> testdata = new HashMap<>();}
建立Config物件,與yaml檔案建立對映關係,讀取資料,通過@MethodSource完成資料的引數化傳遞
public class TestSteps { @ParameterizedTest @MethodSource("YamlData") public void search(String searchInfo,String exceptName) { String name = searchpage.inputSearchInfo(searchInfo).getAll().get(0); assertThat(name,equalTo(exceptName)); } static Stream<Arguments> YamlData() throws IOException { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); Config data = mapper.readValue(Config.class.getResourceAsStream("/demo2.yaml"), Config.class); List<Arguments> list = new ArrayList<>(); Arguments arguments = null; for (String key : data.testdata.keySet()) { Object value = data.testdata.get(key); arguments = arguments(key, value); list.add(arguments); } return Stream.of(list.get(0),list.get(1),list.get(2)); }
為了保證執行通過,可以先簡單列印驗證一下:
5.2 測試步驟的資料驅動
對於測試步驟的資料驅動主要針對兩點:
定位符:我們做App自動化的時候可以把定位符合定位器直接寫在PO中,也可以將其剝離出來,寫在類似yaml的檔案中,定義好格式個物件的對映關係即可完成定位符的資料驅動。行為流:與定位符的剝離思想一致,行為流原本也是寫在PO中的各個方法,這些行為流和定位符是緊密關聯的,因此也可以剝離出來,和定位符在一起組成測試步驟的資料驅動。好比下面這樣的,以雪球App的搜尋場景為例:
public class SearchPage extends BasePage{ //定位符 private By inputBox = By.id("search_input_text"); private By clickStock = By.id("name"); private By cancel = By.id("action_close"); //行為流 //搜尋股票 public SearchPage search(String sendText){ sendKeys(inputBox,sendText); click(clickStock); return this; } //取消返回 public App cancel(){ click(cancel); return new App(); }}
5.2.1 設計思路注:測試步驟的資料驅動是指把PO中變化的量剝離出來,不是對用例裡的呼叫步驟進行封裝。在上面已經提到過不要在測試用例內完成大量的資料驅動:用例通過PO的呼叫是能夠非常清晰展現出業務執行場景的,業務才是用例的核心;
一旦在用例裡使用了大量資料驅動,如呼叫各種 yaml、csv 等資料檔案,會造成用例可讀性變差,維護複雜度變高;
首先來考慮我們的剝離到yaml中的資料結構
做測試步驟的資料局驅動我們希望可以將一個用例中的步驟方法清晰的展示出來,在對應的方法中包括了方法對應的定位符和行為流,這樣能和PO中的結構保持一致,更易讀易維護;如下:search: steps: - id: search_input_text send: pdd - id: namecancel: steps: - id: action_close
另外我們還要考慮擴充套件性,之前提到了還有測試斷言的資料驅動,另外還有一點沒提到的是,框架的健壯程度還要考慮被測系統(Android,IOS)的通用性、版本變更、元素定位符的多樣性等。這樣考慮的話就應該有多個分類,一個分類中包含了PO中的所有方法,一個分類中包含了版本、系統等資訊等,如下(SearchPage.yaml):
#方法methods: search: steps: - id: search_input_text send: pdd - id: name cancel: steps: - id: action_close#定位符對應系統、版本資訊elements: search_input_text: element: ...#斷言asserts: search: assert: ... cancel: assert: ...
按照上述的思路,以搜尋步驟為例,我們需要一個Model類,用來對映不同的資料模組(方法、版本、斷言),對不同的模組需要一一對應的類,類的成員變數結構與yaml檔案中的結構保持一致:1)建立PageObjectModel類
import java.util.HashMap;public class PageObjectModel { public HashMap<String, PageObjectMethod> methods = new HashMap<>(); public HashMap<String, PageObjectElement> elements = new HashMap<>(); public HashMap<String, PageObjectAssert> asserts = new HashMap<>();}
2)建立對應資料模組的類PageObjectMethod
public class PageObjectMethod { public List<HashMap<String, String>> getSteps() { return steps; } public void setSteps(List<HashMap<String, String>> steps) { this.steps = steps; } public List<HashMap<String,String>> steps = new ArrayList<>();}
3)實現解析yaml資料的方法,完成PO中行為流的封裝;
首先按照之前介紹過的通過jackson來解析yaml資料,我們需要檔案的地址,另外我們還需要知道當前執行的方法,用來去yaml中取方法對應的定位符和行為流,所以初步設想應該有method和path兩個引數:public void parseSteps(String method,String path){ ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); try { PageObjectModel model = mapper.readValue(BasePage.class.getResourceAsStream(path),PageObjectModel.class); parseStepsFromYaml(model.methods.get(method)); }catch (IOException e) { e.printStackTrace(); } }
上面的方法中可以看到呼叫了一個parseStepsFromYaml方法,這個方法是將從yaml中獲取到的資料進行處理,拿到對應方法的定位符再拿到定位符緊跟的行為流完成對應的操作步驟(點選、輸入、獲取屬性等);之所以將這個方法單獨抽離出來,是因為後面會對parseSteps過載,方便複用,後面會介紹到。如下:我們要通過methods裡的search方法拿到對應的步驟steps裡的id,在根據id下的send值進行輸入操作methods: search: steps: - id: search_input_text send: pdd - id: name
private void parseStepsFromYaml(PageObjectMethod steps){ //獲取方法名method steps.getSteps().forEach(step ->{ WebElement element = null; if (step.get("id") != null){ element = findElement(By.id(id)); }else if (step.get("xpath") != null){ element = findElement(By.id(step.get("xpath"))); }else if (step.get("aid") != null){ element = findElement(MobileBy.AccessibilityId(step.get("aid"))); if (step.get("send") != null){ element.sendKeys(step.get("send")); }else if (step.get("get") != null){ findElement(by).getAttribute(get); } else { element.click(); //預設操作是點選 } }); }
4)這個時候再回到我們的PO裡,就變成了這個樣子,看一下PO是不是一下子變得簡潔了許多:
public class SearchPage extends BasePage{ //行為流 //搜尋股票 public SearchPage search(String sendText){ parseSteps("search","/com.xueqiu.app/page/SearchPage.yaml"); return this; } //取消返回 public App cancel(){ parseSteps("cancel","/com.xueqiu.app/page/SearchPage.yaml"); return new App(); }}
到這裡,測試步驟的資料驅動算是完成了一個基本模板,還有很多可以優化的地方,比如上面的SearchPage的PO中,parseSteps的兩個引數method和path都是有規律可循的:
method和當前執行的方法名是定義好保持一致的當前PO所對應的yaml檔案的path是固定的下面針對這個點做個小優化
5.2.2 框架優化先來解決方法名method的問題,來看Thread的一個方法:Thread.currentThread().getStackTrace()利用這個方法可以打印出當前方法執行的全部過程,寫單測來驗證,將每一步的方法名都打印出來:這裡將會對上一節中的 parseSteps 方法進行優化,減少重複性工作。
void testMethod(){ Arrays.stream(Thread.currentThread().getStackTrace()).forEach(stack ->{ System.out.println(stack.getMethodName()); }); System.out.println("當前呼叫我的方法是:"+Thread.currentThread().getStackTrace()[2].getMethodName()); }@Testvoid getMethodName(){ testMethod(); }
執行結果:
getStackTracetestMethod //當前執行的方法getMethodName //呼叫testMethod的方法invoke0invokeinvokeinvokeinvokeMethodproceed//...這裡省略中間很多不重要的部分executeexecutestartRunnerWithArgsstartRunnerWithArgsprepareStreamsAndStartmain當前執行的方法是:getMethodName
從結果中可以看到,當方法被呼叫時,呼叫它的方法名會在輸出結果的索引2位置,因此通過此方法就可以成功的拿到我們所需的method引數
再來解決yaml檔案路徑的path引數,這裡可以藉助java.lang.Class.getCanonicalName()方法,此方法可以返回當前類名,包括類所在的包名,如下:@Testvoid getPath(){ System.out.println(this.getClass().getCanonicalName());}//列印結果com.xueqiu.app.testcase.TestSteps
稍加改造就可以變成地址資訊:
@Testvoid getPath(){ System.out.println(this.getClass().getCanonicalName()); String path = "/com.xueqiu.app" + this.getClass().getCanonicalName().split("app")[1].replace(".", "/") + ".yaml"; System.out.println(path);}
列印結果:
com.xueqiu.app.testcase.TestSteps/com.xueqiu.app/testcase/TestSteps.yaml
這樣我們就將當前類的資訊轉變成了一個地址資訊,後面我們只需要將對應的yaml檔案以和類相同的命名,相同路徑結構存放在resources目錄下即可
現在method和path引數的問題都解決了,在來看現在的parseSteps方法://解析步驟public void parseSteps(String method) { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); String path = "/com.xueqiu.app" + this.getClass().getCanonicalName().split("app")[1].replace(".", "/") + ".yaml"; try { PageObjectModel model = mapper.readValue(this.getClass().getResourceAsStream(path),PageObjectModel.class); parseStepsFromYaml(model.methods.get(method)); }catch (IOException e) { e.printStackTrace(); }}public void parseSteps(){ String method = Thread.currentThread().getStackTrace()[2].getMethodName(); parseSteps(method);}
此時再次回到 SearchPage的PO中,可以看到更加的簡潔了,甚至變成了“傻瓜操作”:public class SearchPage extends BasePage{ public SearchPage search(){ parseSteps(); return this; } public App cancel(){ parseSteps(); return new App(); }}
send引數化處理看似好像大功告成,又出現了新的問題,不知道大家注意到沒有,search方法其實是需要send值的,而現在我們的send值是寫死在yaml中的,這反而違背了我們引數化和資料驅動的原則:
methods: search: steps: - id: search_input_text send: pdd #send的內容被寫死在了這裡 - id: name
所以我們需要繼續解決這個問題,將send的值進行引數化
1) 既然是引數化,那就要把send的值變成引數,這裡用$sendText來表示是引數
methods: search: steps: - id: search_input_text # send: pdd send: $sendText #表示引數化 - id: name
2)在search方法中使用HashMap將用例傳遞過來的測試資料儲存至其中,用來傳遞到parseSteps步驟解析方法中。
public SearchPage search(String sendText){ HashMap<String,Object> map = new HashMap<>(); map.put("sendText",sendText); setParams(map); parseSteps(); return this;}
3)再在parseSteps方法所處的類中新增HashMap型別的params變數,用來接收PO傳過來的sendText測試資料
private static HashMap<String,Object> params = new HashMap<>();public HashMap<String, Object> getParams() { return params;}//測試步驟引數化public void setParams(HashMap<String, Object> params) { this.params = params;}
4)最後修改parseStepsFromYaml方法中的send值獲取方式,將佔位的引數$sendText替換成實際傳遞過來的測試資料sendText
if (step.get("send") != null){ String send = step.get("send").replace("$sendText",params.get("sendText").toString()); element.sendKeys(send);}
getAttribute實現在文章前面提到過獲取元素屬性,在自動化測試過程中,經常要獲取元素屬性來作為方法的返回值,以供我們進行其他操作或斷言,其中text是我們最常獲取的屬性,下面來實現此方法的資料驅動
在上面的搜尋股票場景下,加上一步獲取股票的價格資訊
先看一下思路,按照之前的設計,在yaml中的定位符後面跟著的就是行為流,假定有一個getCurrentPrice方法,通過get text來獲取text屬性,寫法如下:getCurrentPrice: steps: - id: current_price get: text
這個時候就可以在parseStepsFromYaml方法中加入屬性獲取的解析邏輯,通過get來傳遞要獲取的屬性
if (step.get("send") != null){ String send = step.get("send").replace("$sendText",params.get("sendText").toString()); element.sendKeys(send);}else if (step.get("get") != null){ String attribute = element.getAttribute(step.get("get")); }
接著我們到SearchPage的PO中實現getCurrentPrice方法,這個時候就會發現一個問題:public Double getCurrentPrice(){ parseSteps(); // return ???; }
沒錯,text屬性獲取到了,可以沒有回傳出來,getCurrentPrice方法沒有return值;我們要將parseStepsFromYaml獲取到的屬性值通過一個“中間商"給傳遞到getCurrentPrice方法中,然後再return到用例中供我們斷言使用
1)產生市場需求,yaml中定義好資料結構
methods: search: steps: - id: search_input_text send: $sendText - id: name getCurrentPrice: steps: - id: current_price get: text dump: price cancel: steps: - id: action_close
2) 實現“中間商”,這個“中間商”就是一個HashMap,將它取名為result
private static HashMap<String,Object> result = new HashMap<>();//測試步驟結果讀取public static HashMap<String, Object> getResult() { return result;}
3)供應商根據市場需求產生產品並提供給中間商,獲取屬性並將屬性值存入result
if (step.get("send") != null){ String send = step.get("send").replace("$sendText",params.get("sendText").toString()); element.sendKeys(send);}else if (step.get("get") != null){ String attribute = element.getAttribute(step.get("get")); result.put(step.get("dump"),attribute); }
4)消費者根據自己的需求去中間商那裡拿到商品,從result中get到price的值
public Double getCurrentPrice(){ parseSteps(); return Double.valueOf(getResult().get("price").toString());}
這樣就成功完成了這個交易場景的閉環,股票價格price被成功返回至用例中
5.3 斷言的資料驅動
因為每個測試資料在傳入用例跑完後,都會對應有斷言來進行結構判定,因此將測試資料對應的斷言資料在一個yaml檔案中,寫入一個數組裡,再同測試資料一起獲取傳入到用例中有了上面的鋪墊,斷言的資料驅動就顯得簡單多了,我個人有時候也簡單的把它歸為測試資料的驅動中
- - didi - 100d- - alibaba - 120d- - sougou - 80d
回到最初的測試資料資料驅動,把資料獲取傳入
@ParameterizedTest@MethodSource("searchYamlData")void search(String searchInfo,String exceptPrice ){ Double currentPrice = searchPage.search(searchInfo).getCurrentPrice(); assertThat(currentPrice,greaterThanOrEqualTo(Double.parseDouble(exceptPrice)));}static Stream<Arguments> searchYamlData() throws IOException { Arguments arguments = null; List<Arguments> list = new ArrayList<>(); ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); String path1 = "/com.xueqiu.app" + TestSearch.class.getCanonicalName().split("app")[1].replace(".","/") + ".yaml"; Object[][] searchData = mapper.readValue(TestSearch.class.getResourceAsStream(path1), Object[][].class); for (Object[] entrySet : Arrays.asList(searchData)){ String key = Arrays.asList(entrySet).get(0).toString(); String value = Arrays.asList(entrySet).get(1).toString(); arguments = arguments(key,value); list.add(arguments); } return Stream.of(list.get(0),list.get(1),list.get(2));}
注:其實這裡應該說還是測試資料驅動,並不能算是斷言的驅動,真正想做成斷言的驅動還需要封裝類似測試步驟驅動的形式。目前沒有做這層封裝,因為在使用中發現斷言的型別很多,直接在用例裡面寫也很方便易讀,加上目前時間精力也有限,待後續需要的時候再繼續補充~
6. 執行效果
用例執行結果:說的再多,不如實際跑一下,檢驗一下框架封裝後的實際執行效果
折騰了這麼久,總算是“大功告成”了,之所以加個引號,是因為這個僅僅是個開始,只能算是初具雛形,像文章中提到的被測系統切換、版本切換、多元素查詢等都還未實現,後續會持續學習更新。