.netcore持續集成測試篇之web項目驗收測試
- 2019 年 10 月 3 日
- 筆記
通過前面的單元測試,我們能夠保證項目的基本模塊功能邏輯是正常的,通過集成測試能夠保證接口的請求是正常的.然而最終項目交付我們還需要對項目進行頁面的行為進行測試,比如頁面布局是否正常,按鈕是否能點擊,點擊後執行的動作是否正確,鏈接是否正常等功能進行測試,表單提交是否返回正確結果等.這些都是一些墨盒測試,一般是由專門測試人員來完成,然而隨着web的發展,各種自動化工具越來越完善,有一些頁面功能的測試也可以由程序員來編寫自動測試代碼完成.這裡主要結合Selenium來介紹如何完成頁面行為的測試.
點擊按鈕
前面我們已經講到如何安裝和簡單使用Selenium,這裡不再介紹.下面介紹一下如何使用Selenium來觸發一個按鈕點擊事件.
首先我們在HelloWorldController里新建Action FormTest(也可以在其它控制器里創建,這裡隨意),代碼如下
public IActionResult FormTest() { return View(); } [HttpPost] public IActionResult FormTest(string name) { return Content(name); }
以上代碼非常簡單,我們創建FormTest並請求自己,然後把請求的數據返回
我們為這個Action新建一個頁面,並且引入jquery.
頁面代碼如下
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <script src="~/js/JQueryt.js"></script> <title>FormTest</title> </head> <body> <form method="post" id="frm1"> <input id="btn1" type="button" value="點我"/> </form> <script> $("#btn1").click(function() { alert("hello,world"); }); </script> </body> </html>
這個頁面里有一個btn1,如果我們點擊它就會彈出一個alert框.
測試代碼如下
[Fact] public void ClickTest() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var element = driver.FindElement(By.Id("btn1")); element.Click(); }
我們先通過id找到這個按鈕,然後令它觸發一個click事件.我們運行測試
我們並沒有手動點擊按鈕,但是彈出了上面的彈框,說明點擊事件正確觸發了.
自動填寫表單
通過以上代碼我們可以看到,觸發一個按鈕點擊事件在Selenium是非常容易的,這對我們自動模擬表單提交提供了大大的便利.Selenium還可以模擬自動填寫表單,思路和上面是一樣的,首先獲取到要填寫的表單,然後模擬填寫內容.下面我們改動一下網頁代碼,在form裏面添加一個簡單的表單
<input type="text" name="name"/>
測試代碼改為如下:
[Fact] public void ClickTest() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var input = driver.FindElement(By.Name("name")); input.SendKeys("hello,world"); }
以上代通過FindElement By.Name獲取到頁面里name為name的元素(聽起來有點繞),然後通過SendKeys方法模擬向指定元素填寫內容
頁面的開以後便會自動填寫以上內容.這樣我們就可以自動填寫內容,然後點擊點我按鈕提交表單了.
自動填寫表單,然後提交
綜合以上我們模擬一次自動填寫表單,然後提交的動作.
下面貼出修改後的完整代碼.
<html> <head> <meta name="viewport" content="width=device-width" /> <script src="~/js/JQueryt.js"></script> <title>FormTest</title> </head> <body> <form method="post" id="frm1"> <input type="text" name="name"/> <input id="btn1" type="button" value="點我"/> </form> <script> $("#btn1").click(function() { $.ajax({ type: "POST", url: "/HelloWorld/FormTest", data: $("#frm1").serialize(), dataType: "text", success: function (response) { alert("返回的數據是:"+response); } }); }); </script> </body> </html>
這次當按鈕點擊以後我們觸發一次ajax提交,然後alert服務器返回的數據
測試代碼如下:
[Fact] public void ClickTest() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var input = driver.FindElement(By.Name("name")); input.SendKeys("hello,world"); var btn = driver.FindElement(By.Id("btn1")); btn.Click(); }
上面的代碼執行了兩個動作,第一是模擬填寫表單數據,第二是點擊按鈕,提交表單.
我們運行測試代碼,看一下結果
可以看到表單自動提交了.
獲取Alert框
我們前面都是通過截圖來看指定的行為是否產生了正確的結果,然而在自動化環境中這是不能接受的,更多的時候我們是在無頭模式下進行測試,然後自動獲取行為產生的結果,然後斷言此結果是否是期待的一個值.下面我們改造以上代碼,自動獲致到Alert框並取得它裏面的值,然後斷言這個值是我們想要的值.
[Fact] public void ClickTest() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var input = driver.FindElement(By.Name("name")); input.SendKeys("hello,world"); var btn = driver.FindElement(By.Id("btn1")); btn.Click(); Thread.Sleep(3000); var alert = driver.SwitchTo().Alert(); var txt = alert.Text; Assert.Equal("返回的數據是:hello,world", txt); }
以上代碼的關鍵是通過SwitchTo獲取到Alert框,進而獲取到它的Text值,我們在ajax請求成功的處理是"返回的數據是:"
+提交的值,因此如果正常則以上代碼會執行成功.這樣我們就不用守着頁面查看結果了.
獲取自定義彈出層
做到以上並沒有成事大吉,實際業務中我們很少使用瀏覽器自帶的Alert,而是使用一些第三方的組件,因為原生Alert用戶體驗實在不是太好,只能在測試的時候玩一玩還可以.由於第三方組件實現方式不同,這就導致獲取的方法也不一樣,我們還要根據具體情況而定.下面我們結合layui的alert框來介紹一下如何來獲取它裏面的內容.
我們在項目中引入layui,然後把ajax請求成功後的alert換成layui的alert,代碼如下
<html> <head> <meta name="viewport" content="width=device-width" /> <link href="~/lib/layui/css/layui.css" rel="stylesheet" /> <script src="~/lib/jquery/dist/jquery.js"></script> <script src="~/lib/layui/layui.all.js"></script> <title>FormTest</title> </head> <body> <form method="post" id="frm1"> <input type="text" name="name"/> <input id="btn1" type="button" value="點我"/> </form> <script> var layer = layui.layer; $("#btn1").click(function() { $.ajax({ type: "POST", url: "/HelloWorld/FormTest", data: $("#frm1").serialize(), dataType: "text", success: function (response) { layer.alert("返回的數據是:" + response); } }); }); </script> </body> </html>
由於這是一個自定義alert,我們先運行一下項目,然後手動點擊下按鈕,等alert框出來以後我們分析一下它的結構:
我們可以看到,layui的這個alert框實際上是一個div層,由於id是動態生成的,因此我們不能使用,但是它的class是固定的,它包含了兩個class元素,內部彈出的具體內容則是藍色高亮的那個div裏面的內容,它的class也是固定的,我們這裡可以使用class獲取到它們.
下面看測試代碼:
[Fact] public void ClickTest() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var input = driver.FindElement(By.Name("name")); input.SendKeys("hello,world"); var btn = driver.FindElement(By.Id("btn1")); btn.Click(); Thread.Sleep(3000); var layer = driver.FindElement(By.ClassName("layui-layer-dialog")); var content = layer.FindElement(By.ClassName("layui-layer-content")); var text = content.Text; Assert.Equal("返回的數據是:hello,world", text); }
產生我們通過class獲取到這個彈出層元素,然後再通過它找到它的子元素(包含彈出信息文字的div).這裡的sleep前面說過,由於js是異步執行的,因此點擊後並不能馬上獲取到結果,這裡我們sleep一下.
需要特別注意的是,通過By.ClassName獲取到的元素可能不止一下,默認取得的是獲取到的第一個,這在有些時候可能並不能滿足我們的要求(這裡代碼比較少,發生衝突的機率比較小),實際工作中我們一定要想辦法保證元素的惟一性,也就是獲取到的元素確定是我們所需要的.
還有一點需要注意的是第三方的組件實現方式可能會改變導致獲取不到內容,這確實沒有比較好的解決方案.
實際工作中可能還會有更為複雜的行為要去模擬,比如說彈出層是一個帶有tab的面板,我們需要切換到特定的tab去尋找想要的內容,由於這些內容都是非標準實現,因此模擬的難度根據採用框架的複雜度而定,有時候可能特別複雜,但是只要靜下心來分析分析,總是能找到解決方案的.
前面講到了如何填寫表單,點擊按鈕提交表單以及獲取彈出層內容.下面講解一下如何獲取鏈接,彈出頁面,iframe以及高級行為.這裡仍然是以實際應該為主導講解一些最基本最常用的功能,並不求面面俱到,有興趣的同事可以查看官方文檔,第三方博客,書籍等獲取更多知識.
鏈接行為測試
鏈接很多時候可以完成按鈕的功能,但是最常用的是跳到一個新的頁面,下面講一下如何獲取到新的頁面
我們在上節的頁面中添加一個a鏈接,代碼如下:
<a id="clk" href="http://www.baidu.com" target="_blank">飛往百度</a>
以上代碼很簡單,點擊一下a標籤就會出現一個新的百度頁面,我們想要判斷一下是否正確打開了百度頁面,測試代碼如下:
[Fact] public void LinkClick() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var link = driver.FindElement(By.Id("clk")); link.Click(); var hands = driver.WindowHandles; var wind = driver.SwitchTo().Window(hands[1]); var title = wind.Title; Assert.Equal("百度一下,你就知道", title); }
上面代碼主要的功能在於當點擊鏈接以後通過driver.WindowHandles獲取到容器的句柄,需要說明的是這裡的句柄並不是指針類型的句柄,而是一個字符串類類型的變量,我們可以通過它找到指定的窗口,下面部分通過SwitchTo切換到一個窗口(SwitchTo我們前面講到過),Window接收一個字符串類開的參數,雖然提示字符說是窗口的標題,實際上並不是,而是我們剛才獲取到的句柄,我們知道現在共有兩個窗口,百度窗口是後打開的,因此它的索引是是1.然後我們再獲取它的標題,看看是不是"百度一下, 你就知道"
需要說明的是,以上我們雖然是通過索引獲取的百度窗口,這樣可能會因為位置切換造成問題(這裡強烈不建議手動修改自動過程中的行為,實際上真實的測試環境是無頭環境,因此這其實不是一個很大的問題),就上面的例子我們確實有辦法能惟一確定百度窗口,但是如果窗口過多想要不使用索引獲取到指定的窗口還是很困難的,這裡強烈建議如果有打開非常多的窗口的複雜行為時,把測試分成若干個測試,每個測試里的邏輯只打開少量窗口,這樣出現問題也更容易排查.
點擊時按下修改鍵
前面我們多次用到了模擬點擊事件,其實這也是實際項目中用的最多的,但是也不排除少數情況下會用到其它的按鍵,比如說拖拽,雙擊,ctrl+點擊等,
下面我們演示如何在百度首頁點擊百度新聞並在新頁面打開,我們知道百度首頁的新聞默認是在本頁打開的,如果點擊鏈接時按下ctrl鍵則會在新頁面中打開.下面我們模擬ctrl+點擊這個行為
這裡其實也很簡單,主要通過Actions封裝對象來觸發一系列動作來達到我們的目的.
下面看測試代碼:
[Fact] public void LinkClick() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://www.baidu.com"; driver.Navigate(); var link = driver.FindElement(By.LinkText("新聞")); Actions actions = new Actions(driver); actions.KeyDown(Keys.LeftControl).Click(link).Build().Perform(); }
以上代碼主要是通過傳入driver對象構造一個Actions類型對象,這個對象在調用build之前會一直返回自身,類似是是jQuery里的鏈式操作,這樣我們就可以連續執行多個動作.
下面的代碼我們先是調用actions對象的keyDown方法,然後傳入要按下的鍵,然後再調用點擊事件,最後調用Build方法終止鏈式調用,最後再執行Perform執行前面的操作.啟動測試就會發現瀏覽器在新的頁面打開了百度新聞頁
上面用到了一個以前沒用到的選擇方法那就是By.LinkText,語義非常明確那就是根據鏈接的文本找到鏈接對象.
Iframe對象的獲取
我們知道Iframe對象的處理比較麻煩,裏面是一個比較封裝的區域與外面通信過程比較麻煩,在selenium里它的處理也比較特殊,直接按照id或者其它特徵獲取到它幾乎沒有任何作用,因為無法獲取到內容元素,selenium是通過switchTo.Frame傳入獲取到的iframe對象對它進行一層封裝,然後就能夠正常獲取到它內部的元素了.
我們在頁面添加一個簡單的iframe頁面,代碼如下
<iframe id="ifrm1" src="http://www.baidu.com"></iframe>
測試代碼如下
[Fact] public void FindIframe() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var ele = driver.FindElement(By.Id("ifrm1")); var frm = driver.SwitchTo().Frame(ele); var txt = frm.FindElement(By.LinkText("新聞")).Text; Assert.Equal("新聞", txt); }
以上代碼首先像獲取普通元素一樣獲取到這個iframe對象,然後通過SwitchTo.Frame把它傳入封裝成個frame對象,後面就可以獲取到它內部的元素了.(iframe指向百度首頁,我們獲取到新聞鏈接)
然而實際項目中,往往我們並不自己去創建iframe,而是由一些第三方ui框架自動創建的,框架生成的iframe要麼沒有id,要麼是動態的,因此使用自動生成元素的id要非常慎重,但是筆者見過不少在生成iframe時可以給iframe指定name的,由於頁面中iframe一般都不會太多,我們可以給它命一個惟一的名字,通過名字找到它.如果沒有名字,還可以根據它的class找到它,一般iframe樣式class都是固定的,但是這時候要想辦法確保選擇到的對象是惟一的,這樣才能保證測試結果的穩定性.
獲取下拉列表
在一些查詢功能中,往往全有下拉列表,通過js獲取或者設置下拉項並不是一件很困難的事,然而我們並不想不了測試而增加無關的js代碼,這樣用完還要刪除非常麻煩,其實Selenium也提供了設置下拉列表選項的功能,這樣極大方便了我們的測試.
下面看示例代碼
我們首先 在頁面中添加如下代碼
<select name="China" id="zhengzhouDistrict"> <option value="">--請選擇區域--</option> <option value="pdditrict">中原區</option> <option value="hpditrict">二七區</option> <option value="xhditrict">管城區</option> <option value="cnditrict">高新區</option> <option value="sjditrict">開發區</option> </select>
測試代碼如下:
[Fact] public void DropDownListTest() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var element = driver.FindElement(By.Id("zhengzhouDistrict")); SelectElement dropdownList = new SelectElement(element); dropdownList.SelectByIndex(3); var select = dropdownList.SelectedOption.Text; Assert.Equal("管城區", select); }
以上代碼中我們先是通過普通方法獲取到了這個下拉列表,然後把它封裝成一個SelectElement對象,然後調用它的SelectByIndex設置選中的項,這樣選中的項就是不默認值了,而是我們想要選擇的值.
select還有按索引,鍵,值等設置選擇項的方法,並且可以取消選擇,大家自己嘗試一下,這裡不再介紹.
然而以上方法並沒有什麼太大作用,由於瀏覽器自帶的select界面往往都不太美觀,並且動態交互性不是非常好,實際項目中我們很少使用原生的select,而是使用第三方ui框架帶的select,而第三方框架往往都是把select隱藏起來,然後把它的值賦值給一個input元素,它設置和獲取值都是通過第三方框架提供的api而非原生select自帶的方法.如果這時候使用以上方法獲取select元素就會導致失敗,selenium會提交元素被隱藏無法交互.針對這個問題筆者採用了一種比較笨的方法那就是模擬按鍵,當然這裡模擬按鍵並不引入第三方按鍵類框架,而是使用selenium本身的功能.
下面仍然以layui為例說明如何設置下拉列表值.
頁面代碼如下
<html> <head> <meta name="viewport" content="width=device-width" /> <link href="~/lib/layui/css/layui.css" rel="stylesheet" /> <script src="~/lib/jquery/dist/jquery.js"></script> <script src="~/lib/layui/layui.all.js"></script> <title>FormTest</title> </head> <body> <form class="layui-form" action=""> <div class="layui-form-item"> <div class="layui-input-inline"> <select name="China" id="zhengzhouDistrict"> <option value="">--請選擇區域--</option> <option value="pdditrict">中原區</option> <option value="hpditrict">二七區</option> <option value="xhditrict">管城區</option> <option value="cnditrict">高新區</option> <option value="sjditrict">開發區</option> </select> </div> </div> </form> <script> var layer = layui.layer; var form = layui.form; form.render(); </script> </body> </html>
以上代碼和上面的類似,只是這裡把它封裝成layui的select,我們的思路是先獲取到layui的顯示select的元素,也就是最終渲染的input元素,經過觀察發現這個元素有一個這樣的樣式layui-input,我們可以通過這個關鍵信息找到它,然後點擊一下,這時候下拉列表就出來了,此時再點擊向下按鈕,在想要的位置處click一下就可以得到想要的結果了.
測試代碼如下:
[Fact] public void DropDownListTest() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var input = driver.FindElement(By.ClassName("layui-input")); input.Click(); Actions action = new Actions(driver); action.SendKeys(Keys.ArrowDown+Keys.ArrowDown+Keys.ArrowDown).Click().Build().Perform(); }
以上主要是使用的Actions連續點擊兩次向下,就可以選擇到指定的元素了.
日期框處理
這裡結合layui來講解如何處理日期框的問題
首先我們來觀察一下layui日期輸入框的特點,它其實是一個input,並且是可以接受用戶輸入的,這就跟我們模擬手動輸入帶來了方便,但是事情並沒有這麼簡單,我們可以看到手輸內容之後還要點擊那個彈出層的確定按鈕來確認輸入,一旦點擊了確定則會把彈出層默認選中的日期輸入到input框中,覆蓋了剛才的選擇.然而它卻有以下一個特點:如果我們輸入以後不點擊確認,而是把光標移到空白色點擊或者光標焦點移到其它可輸入元素內,則也可以確認輸入.這樣我們就可以在日期輸入框輸入內容以後再把焦點移到其它輸入框就能夠確認輸入了.
頁面代碼如下:
<html> <head> <meta name="viewport" content="width=device-width" /> <link href="~/lib/layui/css/layui.css" rel="stylesheet" /> <script src="~/lib/jquery/dist/jquery.js"></script> <script src="~/lib/layui/layui.all.js"></script> <title>FormTest</title> </head> <body> <div class="layui-inline"> <input type="text" class="layui-input" id="test1"> <input type="text" id="input1"/> </div> <script> var laydate = layui.laydate; laydate.render({ elem: '#test1' }); </script> </body> </html>
以上有兩個input框,一個是普通input,一個是日期框,我們模擬在日期輸入框輸入內容後把焦點移動它右邊空白處點擊,然後看看上面日期輸入框里的值是不是我們賦予的.
測試代碼如下:
[Fact] public void DropDownListTest() { IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory); driver.Url = "http://localhost:28614/HelloWorld/FormTest"; driver.Navigate(); var input = driver.FindElement(By.Id("test1")); input.SendKeys("2008-5-3"); var position = input.Location; var clickposition = position.X + 200; Actions actions = new Actions(driver); actions.MoveByOffset(clickposition, 0).Click().Build().Perform(); var txt = input.GetAttribute("value"); Assert.Equal("2008-05-03", txt); }
以上代碼我們首先獲取到這個日期選擇框,然後給它輸入值.下面我們獲取它的位置主要是為了讓它關閉(如果點擊了空白處或者其它可點擊控件,則日期選擇框就會消失,這裡只所以要關閉它是因為它當前處於激活狀態,如果不關閉則會影響其它操作).我們獲取到它的位置後向右移動鼠標到空白處,然後點擊空白處日期選擇框就消失了.我們輸入的是’2008-5-3’而斷言它是’2008-05-03’是因為layui有格式糾正功能,自動把一位的數據前面補零.
這裡獲取input的值是通過GetAttribute獲取的,而不是通過Text,text是獲取元素內部的文本(也就是文本包含在標籤里),而input的值是它的value屬性的一個值,因此使用Text獲取不到.
一定要注意點擊位置,如果點擊位置位於鏈接或者提交按鈕上則可能觸發不可預期的效果.
前面我們介紹了如何通過普通的方法給元素設置值以及模擬特定的行為,本篇主要介紹如何獲取頁面cookie,如何模擬手機測試.
獲取頁面cookie
有些比較複雜的測試可能會用到cookie,在Selenium里通過driver.Manage().Cookies
就可以獲取到頁面所有的cookie對象了.
模擬手機瀏覽器
由於目前沒有手機項目,這裡並不詳細介紹,只是作為一個知識點簡單介紹一下.
看以下測試代碼
[Fact] public void DropDownListTest() { ChromeOptions opts = new ChromeOptions(); opts.EnableMobileEmulation("iPhone X"); IWebDriver driver = new ChromeDriver(Environment.CurrentDirectory,opts); driver.Url = "http://www.baidu.com"; driver.Navigate(); }
這裡主要是增加一個谷歌瀏覽器啟用模擬手機瀏覽器選項,並指定一個模擬器的名字(這些名字可以通過谷歌瀏覽器的手機模式查看).然後再啟動頁面就會在指定的手機模擬器運行了.
以上運行結果截圖如下: