[小北De编程手记] : Lesson 06 - Selenium For C# 之 流程控制
无论你是用哪一种自动化测试的驱动框架,当我们构建一个复杂应用程序的自动化测试的时候。都希望构建一个测试流程稳定,维护成本较低的自动化测试。但是,现实往往没有理想丰满。而这一篇,我会为大家讲解我们在使用Selenium进行Web测试的时候应该如何控制我们的测试流程,从而尽可能地提高自动化测试可维护性。那么,先看一下这一篇的内容主要涉及到的话题:
- 自动化测试的成本
- 隐式的等待同步策略
- 显式的等待同步策略
- 自定义等待同步策略(一些关于自动化框架设计的探讨)
(一)自动化测试的成本
《Selenium For C#》的系列文章写到这里,我觉得是时候跟大家分享一下关于自动化测试维护成本的问题了。对于自动化测试带来的好处我就不多说了,网上对它推崇之词犹如滔滔江水连绵不绝。这里我想提一下构建一个自动化测试的成本是什么?如果你的公司或是团队想要为自己的产品构建完整的自动化测试。那么,我接下来要描述的这些都将会是你需要付出的成本。当然,与此同时你也会得到自动化测试带来的好处。
- 人员成本:自动化测试的构建,势必需要一些技术要求较高的架构人员来完成。对于框架的稳定性、易用性、安全、集成部署... ...等因素的要求,都是远高于开发框架的。
- 开发成本:这一部分的成本,往往是最明显的。自动化测试的开发量几乎等同于待测试系统的开发量。
- 维护成本:自动化构建的所有成本中,维护成本往往是最难预测的,过高的维护成本会直接导致自动化构建的失败。
我见过一些公司构建的自动化测试,每一轮运行会花费10个小时左右的时间。会有大量的case失败,但其中大部分都是Case本身的原因而非Bug引起的。这就直接导致了QA需要花费大量的时间在调试失败的Case上。此时,我们再想想当初构建自动化测试的初衷,是为了节约人力成本,让人力成本可以花在更加有效的地方。但上述这样的自动化构建真是实现了我们最初的目的吗?所以,你所构建的自动化测试平台的质量直接关系到此次构建的成败。关于这个话题也不是一两句能说明白的,如果有时间,我会写一个关于自动化测试平台构建的系列,罗列一下自认为较好的自动化测试的具体实践。在此,只是先抛出我个人的一个观点:“如果你的自动化测试维护成本远远高于手动测试的成本。那么,我想这样的自动化测试构建是需要慎重考量的,或者说是失败的!”。
(二)等待同步策略
本系列的文章虽然只是对Selenium技术做一些讲解,但是等待同步策略是每一个自动化框架的设计人员都应该了解的。而且在具体的实践中,由于等待同步策略的使用不当而导致Case失败的例子也不在少数。
在实际测试运行时,测试可能并不是以相同的速度响应。例如,一个进度条会等待几秒才会100%,页面上的某个图表需要加载一段时间才会显示出来。随着Web技术的发展,更多的延迟加载、Ajax以及RealTime技术的广泛使用。对自动化测试Case提出了更高的要求。对于接触过自动化测试,或是参与过自动化测试实践的同学应该会有这样经验:很多自动化测试的用例在调试的时候没有问题,但是一旦在CI集成系统中运行就会出现元素找不到的异常(多数是因为元素还没有来得及加载)。遇见这种情况,多数会把问题归结于环境的影响。但是,也有可能是测试用例本身不够健壮。下面我来给大家介绍一下Selenium框架为我们提供的等待机制,希望对各位小伙伴书写健壮的测试用例提供一些帮助。
(三)隐式的等待同步策略
Selenium WebDriver提供了隐式的等待策略,我们可以设置一个超时时间。如果WebDriver没有在DOM中找到元素(不会马上抛出异常),它将继续等待直到超过了设定的隐式等待时间,才会抛出找不到元素的异常,隐式的等待同步设置很简单,具体的设置方式如下所示:
1 /// <summary> 2 /// demo1 : 设置等待同步策略 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.TestFlowControl.Demo1")] 5 public void TestFlowControl_Demo1() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 // 1. 隐式的等待 同步测试 9 driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(10)); 10 11 driver.Close(); 12 }
WebDriver的Manage().Timeouts() 返回的是一个实现了ITimeouts接口的对象。这里我们可以顺便了解一下该接口定义的其他方法:
- ImplicitlyWait:设置默认的等待时间。
- SetPageLoadTimeout:设置默认的页面加载超时时间。
- SetScriptTimeout:设置JavaScrip执行超时时间。
1 // Summary: 2 // Defines the interface through which the user can define timeouts. 3 public interface ITimeouts 4 { 5 6 ITimeouts ImplicitlyWait(TimeSpan timeToWait); 7 8 ITimeouts SetPageLoadTimeout(TimeSpan timeToWait); 9 10 ITimeouts SetScriptTimeout(TimeSpan timeToWait); 11 }
因此我们可以更细粒度的测试程序的加载时间(虽然这些是性能测试应当关注的问题),可以看到ITimeouts的接口定义的方法也都是自引用的(关于自引用可以参照《Lesson 05 - Selenium For C# 之 API 下》),所以我们可以用这样的方式来调用:
1 /// <summary> 2 /// demo1 : 设置等待同步策略 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.TestFlowControl.Demo1")] 5 public void TestFlowControl_Demo1() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 // 1. 隐式的等待 同步测试 9 driver.Manage().Timeouts() 10 .ImplicitlyWait(TimeSpan.FromSeconds(10)) 11 .SetPageLoadTimeout(TimeSpan.FromSeconds(10)) 12 .SetScriptTimeout(TimeSpan.FromSeconds(10)); 13 14 driver.Close(); 15 }
(四)显示的等待同步策略
设置隐式的等待可以解决元素加载时间导致DOM元素定位失败的问题。但隐式的等待是全局属性,一旦设置了隐式的等待就会影响整个运行计划的时间。所以,更推荐的方式是显式的等待同步策略。尽量的使用显式的等待,Selenium Webdriver也为我们提供了许多更加精准的定位方式,先看一个Demo:
1 /// <summary> 2 /// demo2 :设置显示等待同步策略 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.TestFlowControl.Demo2")] 5 public void TestFlowControl_Demo2() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 //省略操作代码.... 9 WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); 10 wait.Until(ExpectedConditions.ElementExists(By.Id("Object ID"))); 11 }
上述代码是一个显示等待的例子,Selenium WebDriver 提供了WebDriverWait的控制等待相关配置(这里我们设置了等待时间),而后我们使用了对象的Until方法等待,直到某个元素存在为止。Unitl方法的定义如下,他接受一个Func<T,TResult>类型的参数,Func类型是C#3.5之后引入的针对委托类型的简化定义。在此,你可以把它理解为需要传递一个操作方法。而Unitl方法会循环的执行用户传入的方法(这里就是等待元素出现)。直到超时或者操作方法满足下列条件之一:
- 函数返回值不为 null。
- 函数返回值不为 flase。
- 函数抛出的异常不再设置的 ignored exception列表中。(可以用 wait.IgnoreExceptionTypes设置需要忽略的异常)
1 public TResult Until<TResult>(Func<T, TResult> condition);
通常情况下,一般的编写自动化测试Case的人员是不需要直接编写Until的Func参数的执行操作,这部分内容通常是由框架开发人员完成。另外,Selenium WebDriver 已经为我们提供了常见的操作函数,也就是上面Code中用到的ExpectedConditions类,它内部已经集成了大量的方法供框架消费者使用。这些方法的功能已经在命名上有了很好的体现,这里就不一一介绍,我简单的列举一下当前使用的Selenium WebDriver(2.49.0)的版本ExpectedConditions包含方法:
1 // Summary: 2 // Supplies a set of common conditions that can be waited for using OpenQA.Selenium.Support.UI.WebDriverWait. 3 public sealed class ExpectedConditions 4 { 5 public static Func< IWebDriver, IAlert > AlertIsPresent(); 6 public static Func< IWebDriver, bool > AlertState(bool state); 7 public static Func< IWebDriver, IWebElement > ElementExists(By locator); 8 public static Func< IWebDriver, IWebElement > ElementIsVisible(By locator); 9 public static Func< IWebDriver, bool > ElementSelectionStateToBe(By locator, bool selected); 10 public static Func< IWebDriver, bool > ElementSelectionStateToBe(IWebElement element, bool selected); 11 public static Func< IWebDriver, IWebElement > ElementToBeClickable(By locator); 12 public static Func< IWebDriver, IWebElement > ElementToBeClickable(IWebElement element); 13 public static Func< IWebDriver, bool > ElementToBeSelected(By locator); 14 public static Func<IWebDriver, bool> ElementToBeSelected(IWebElement element); 15 public static Func< IWebDriver, bool > ElementToBeSelected(IWebElement element, bool selected); 16 public static Func< IWebDriver, IWebDriver > FrameToBeAvailableAndSwitchToIt(By locator); 17 public static Func< IWebDriver, IWebDriver > FrameToBeAvailableAndSwitchToIt(string frameLocator); 18 public static Func< IWebDriver, bool > InvisibilityOfElementLocated(By locator); 19 public static Func< IWebDriver, bool > InvisibilityOfElementWithText(By locator, string text); 20 public static Func< IWebDriver, ReadOnlyCollection <IWebElement >> PresenceOfAllElementsLocatedBy(By locator); 21 public static Func< IWebDriver, bool > StalenessOf(IWebElement element); 22 public static Func< IWebDriver, bool > TextToBePresentInElement(IWebElement element, string text); 23 public static Func< IWebDriver, bool > TextToBePresentInElementLocated(By locator, string text); 24 public static Func< IWebDriver, bool > TextToBePresentInElementValue(By locator, string text); 25 public static Func< IWebDriver, bool > TextToBePresentInElementValue(IWebElement element, string text); 26 public static Func< IWebDriver, bool > TitleContains(string title); 27 public static Func< IWebDriver, bool > TitleIs(string title); 28 public static Func< IWebDriver, bool > UrlContains(string fraction); 29 public static Func< IWebDriver, bool > UrlMatches(string regex); 30 public static Func< IWebDriver, bool > UrlToBe(string url); 31 public static Func< IWebDriver, ReadOnlyCollection <IWebElement >> VisibilityOfAllElementsLocatedBy(By locator); 32 public static Func< IWebDriver, ReadOnlyCollection <IWebElement >> VisibilityOfAllElementsLocatedBy(ReadOnlyCollection <IWebElement > elements); 33 }
(五)自定义等待同步策略
最后这一部分,其实是大多自动化测试的使用人员不会用到的部分。但对于构建测试框架的架构师而言,这一部分则是必不可少的。实际的应用场景中总会有针对自己程序特定的等待需求。比如:待测试的应用程序在所有的页面异步加载的时候,都会出现一个等待的进度条,也就是说我们的操作需要等待这个进度条消失之后才能进行。那么,对于这部分功能的处理就不应该有上层的测试框架使用人员来完成,框架开发人员应当开发一个简单的接口供上层测试人员使用。关于接口的开发规范,开发方式的讨论已经超出了本系列的范围,这里我只是简单的展示一下如何实现功能,在测试框架开发的相关文章中,我会详细的描述这部分的内容。
首先,我们看看如何做一个简单的扩展,刚刚提到了,Func是一个委托的简化定义,so... ... 我们可以用如下方式来扩展:
1 /// <summary> 2 /// demo3 : 扩展等待 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.SeleniumAPI.Demo3")] 5 public void SeleniumAPI_Demo3() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 //省略操作代码.... 9 WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); 10 wait.Until<bool>( 11 delegate(IWebDriver dir) 12 { 13 var element = dir.FindElement(By.XPath(".//div[@id='divProcessBar']")); 14 return !element.Displayed; 15 } 16 ); 17 }
上面的Code,定义了匿名委托实现了之前描述的针对进度条消失的等待。但是这样的实现方式是需要所有的测试框架使用这都了解委托的概念的,一般来说测试用例编写人员都是来自QA Team的,这样的要求会直接提高对框架使用者的技术要求,同样这样的写法也违背了很多面向对象程序设计的基本原则。更推荐的写法是单独定义一个静态类(这个静态类由框架开发人员维护):
1 /// <summary> 2 /// 自定义的扩展条件 3 /// </summary> 4 public class ExpectedConditionsExtension 5 { 6 /// <summary> 7 /// 等待进度条消失 8 /// </summary> 9 /// <param name="dir">WebDriver对象</param> 10 /// <returns>操作Func对象</returns> 11 public static Func<IWebDriver, bool> ProcessBarDisappears() 12 { 13 return delegate(IWebDriver driver) 14 { 15 IWebElement element = null; 16 try 17 { 18 element = driver.FindElement(By.XPath(".//div[@id='divProcessBar']")); 19 } 20 catch (NoSuchElementException) { return true; } 21 return !element.Displayed; 22 }; 23 } 24 }
现在对于之前上层消费代码的调用就变成了下面的样子:
1 /// <summary> 2 /// demo4 : 扩展等待 升级版 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.TestFlowControl.Demo4")] 5 public void TestFlowControl_Demo4() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 //省略操作代码.... 9 WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); 10 wait.Until(ExpectedConditionsExtension.ProcessBarDisappears()); 11 }
需要说明的是,这不是简单的语法糖(即仅仅简化代码),它带来更多的好处是降低了对框架的使用人员的技术要求。由于这里我们不需要使用框架的人懂得C#的委托(当然懂了更好),所以就降低了框架入门的成本,试想一下如果你是一个测试框架的架构师。你的用户就是你们团队的QA。面对上述的两种方式,我相信后一种调用方式会更容易让他们接受和正确的使用。另一方面,由于做了这样的封装,如果开发人员修改了进度条的结构我们的可控的(只需修改扩展类中的定位)。当然,你也可以要求所有的QA熟练的掌握C#的各种高级语法知识。但如果是那样的话,我们开发测试框架的意义何在?开发测试框架的一个重要目的不就是为了屏蔽复杂的技术实现,降低学习成本吗? 这个思想,也将会是后续关于测试框架设计的主导思想(毕竟测试框架的用户是QA,请不要默认他们具有高级开发级别的能力,这里绝没有说QA的能力不如开发,我只是想说明从设计一个测试框架的角度来讲易用性是一个很重要的指标)。
《Selenium For C#》的相关文章:Click here.
- [小北De编程手记] : Lesson 01 - Selenium For C# 之 环境搭建
- [小北De编程手记] : Lesson 02 - Selenium For C# 之 核心对象
- [小北De编程手记] : Lesson 03 - Selenium For C# 之 元素定位
- [小北De编程手记] : Lesson 04 - Selenium For C# 之 API 上
- [小北De编程手记] : Lesson 05 - Selenium For C# 之 API 下
- [小北De编程手记] : Lesson 06 - Selenium For C# 之 流程控制
- [小北De编程手记] : Lesson 07 - Selenium For C# 之 窗口处理
- [小北De编程手记] : Lesson 08 - Selenium For C# 之 PageFactory & 团队构建
说明:Demo地址:https://github.com/DemoCnblogs/Selenium