.netcore持续集成测试篇之web项目验收测试
通过前面的单元测试,我们能够保证项目的基本模块功能逻辑是正常的,通过集成测试能够保证接口的请求是正常的.然而最终项目交付我们还需要对项目进行页面的行为进行测试,比如页面布局是否正常,按钮是否能点击,点击后执行的动作是否正确,链接是否正常等功能进行测试,表单提交是否返回正确结果等.这些都是一些墨盒测试,一般是由专门测试人员来完成,然而随着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();
}
这里主要是增加一个谷歌浏览器启用模拟手机浏览器选项,并指定一个模拟器的名字(这些名字可以通过谷歌浏览器的手机模式查看).然后再启动页面就会在指定的手机模拟器运行了.
以上运行结果截图如下: