• Selenium Web Test解耦UI变化


    使用Selenium进行Web UI的自动化测试是很好的选择,它支持多种语言来实现你的测试代码,也支持多种浏览器。我选择的是Selenium Web Dirver + C# + FireFox来进行开发,并且采用PageObject design pattern来组织代码,每个page对象使用page factory工厂方法生成。
    下面的示例是描述登录页面的LoginPage类:

     

     1 class LoginPage
     2 {
     3     private IWebDriver driver;
     4  
     5     [FindsBy(How = How.XPath, Using = "//input[@type='text' and @name='userName']")]
     6     private IWebElement txtUserName;
     7  
     8     [FindsBy(How = How.Name, Using = "userName")]
     9     private IWebElement txtPassword;
    10  
    11     [FindsBy(How = How.Name, Using = "login")]
    12     private IWebElement btnLogin;
    13  
    14     public LoginPage(IWebDriver driver)
    15     {
    16         this.driver = driver;
    17     }
    18  
    19     public FindFlightsPage Do(string UserName, string Password)
    20     {
    21         txtUserName.SendKeys(UserName);
    22         txtPasswowrd.SendKeys(Password);
    23         btnLogin.Click();
    24  
    25         return new FindFlightsPage(driver);
    26     }
    27 }

    可以看到,每个IWebElement私有成员都描述一个LoginPage中的元素,并且使用“FindsBy”特性来描述如何定位当前元素在页面中的位置。这样,我们就可以通过Selenium提供的PageFactory类来生成对应的Page对象了:

     

     1 class Program
     2 {
     3     static void Main()
     4     {
     5         IWebDriver driver = new FirefoxDriver();
     6         driver.Navigate().GoToUrl("http://newtours.demoaut.com");
     7  
     8         LoginPage Login = new LoginPage(driver);
     9  
    10         // initialize elements of the LoginPage class
    11         PageFactory.InitElements(driver, Login);
    12         // all elements in the 'WebElements' region are now alive!
    13         // FindElement or FindElements no longer required to locate elements
    14  
    15         FindFlightsPage FindFlights = Login.Do("User", "Pass");
    16         driver.Quit();
    17     }
    18 }

    但是这样实现的LoginPage类,如果被测系统在UI上面有变化,比如元素的ID或Name有变化,页面的结构有变化,我们就需要更改LoginPage类中的“FindsBy”特性的内容,并且重新编译测试代码,重新部署测试代码到测试环境,而且这种情况在项目初期会经常出现,势必会导致每天花费大量时间来重新更换测试环境。

    下面介绍我使用的方法,来解开页面UI的描述信息和Page类之间的耦合。
    这是我写的LoginPage类:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading;
     6  
     7 using OpenQA.Selenium;
     8 using OpenQA.Selenium.Interactions;
     9 using OpenQA.Selenium.Support.PageObjects;
    10 using Selenium.Tools;
    11  
    12 namespace WebTest.TestFramework
    13 {
    14     public class LoginPage : PageBase
    15     {
    16         [NeedRefresh]
    17         public IWebElement UserNameInput { get; set; }
    18         [NeedRefresh]
    19         public IWebElement PassWordInput { get; set; }
    20         public IWebElement SelectLanguageLinkBar { get; set; }
    21         public IWebElement EnglisghLanguageLink { get; set; }
    22  
    23         public LoginPage(IWebDriver driver)
    24         {
    25             this.webDriver = driver;
    26         }
    27  
    28         public void Login(string userName, string passwd)
    29         {
    30             this.UserNameInput.SendKeys(userName);
    31             this.PassWordInput.SendKeys(passwd);
    32             this.UserNameInput.Submit();
    33         }
    34  
    35         public void SelectLanguage(LanguageType type)
    36         {
    37             Actions actions = new Actions(this.webDriver);
    38             actions.MoveToElement(SelectLanguageLinkBar);
    39             actions.MoveToElement(EnglisghLanguageLink);
    40             actions.Click();
    41             actions.Perform();
    42         }
    43     }
    44 }

    抽象出一个PageBase类:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using OpenQA.Selenium;
     6  
     7 namespace AFPWebTest.TestFramework
     8 {
     9     public class PageBase
    10     {
    11         protected IWebDriver webDriver;
    12     }
    13 }

    其中,NeedRefresh特性是自定义的:

    1 using System;
    2  
    3 namespace Selenium.Tools
    4 {
    5     [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    6     public class NeedRefreshAttribute : Attribute
    7     { }
    8 }

    还有一个自定义的Ignore特性:

    1 using System;
    2  
    3 namespace Selenium.Tools
    4 {
    5     [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    6     public class IgnoreAttribute : Attribute
    7     {}
    8 }

    使用NeedRefreshAttribute描述页面元素来表示在初始化Page类之后,这个页面元素的相关属性和值素会有更改。IgnoreAttribute是指在使用PageFactory初始化页面对象时需要略过,不需要实例化的元素。
    下面,我们设计一个xml结构来描述页面中的元素:

     1 <?xml version="1.0"?>
     2 <Map>
     3     <UIMaps>
     4         <UIMap Id="UserNameInput">
     5             <By>Id</By>
     6             <ToFind>userName</ToFind>
     7         </UIMap>
     8         <UIMap Id="PassWordInput">
     9             <By>Id</By>
    10             <ToFind>password</ToFind>
    11         </UIMap>
    12         <UIMap Id="SelectLanguageLinkBar">
    13             <By>XPath</By>
    14             <ToFind>//div[@id='formContainer']//div[@class='header']</ToFind>
    15         </UIMap>
    16         <UIMap Id="EnglisghLanguageLink">
    17             <By>XPath</By>
    18             <ToFind>//div[@id='formContainer']//a[contains(., 'English')]</ToFind>
    19         </UIMap>
    20     </UIMaps>
    21 </Map>

    其中,每个UIMap节点描述一个页面元素,属性Id的值需要和Page类中Public的IWebElement属性名字相同,XML By节点的值和Selenium提供的By类的方法名相同,有这些值:By.ClassName, By.CssSelector, By.Id, By.LinkText, By.Name,By.TagName和By.XPath.XML ToFind节点的值是使用以上By提供的方法所传入的参数。

    下面是xml文件对应的UIMap和Map类的实现:

      

     1 using System.Xml.Serialization;
     2  
     3 namespace Selenium.Tools.xml
     4 {
     5     public class UIMap
     6     {
     7         [XmlAttribute]
     8         public string Id
     9         { get; set; }
    10  
    11         [XmlElement]
    12         public string By
    13         { get; set; }
    14  
    15         [XmlElement]
    16         public string ToFind
    17         { get; set; }
    18     }
    19 }
    20  
    21 using System.Xml.Serialization;
    22  
    23 namespace Selenium.Tools.xml
    24 {
    25     [XmlRoot]
    26     public class Map
    27     {
    28         [XmlArray]
    29         public UIMap[] UIMaps
    30         { get; set; }
    31     }
    32 }

    我们就是通过这个描述Web Page的XML文件,来达到解耦测试代码和UI描述的目的。
    好了,下面就是具体实现PageFactory和解析UIMaps的代码:

     

      1 using System;
      2 using System.Linq;
      3 using System.Reflection;
      4  
      5 using System.Xml.Serialization;
      6 using System.IO;
      7 using System.Collections.Generic;
      8 using Castle.DynamicProxy;
      9 using Selenium.Tools.xml;
     10 using OpenQA.Selenium;
     11 using OpenQA.Selenium.Support.PageObjects;
     12 using OpenQA.Selenium.Internal;
     13  
     14 namespace Selenium.Tools
     15 {
     16     public static class PageFactory
     17     {
     18         public static string UIMapFilePath { get; set; }
     19  
     20         private static bool DoesHasAttribute(PropertyInfo propertyInfo, IList<Type> attributes)
     21         {
     22             foreach (Type attribute in attributes)
     23             {
     24                 var customAttributes = propertyInfo.GetCustomAttributes(attribute, true);
     25                 if (customAttributes.Length != 0)
     26                 {
     27                     return true;
     28                 }
     29             }
     30             return false;
     31         }
     32  
     33         private static Tpage ConstructPage<Tpage>(IWebDriver driver, IList<Type> ignoreAttributes, IList<Type> fetchAttributes) where Tpage : class
     34         { 
     35             Type type = typeof(Tpage);
     36             ConstructorInfo constructorInfo = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public,
     37                 null,
     38                 CallingConventions.HasThis,
     39                 new Type[] { driver.GetType() },
     40                 null);
     41             Tpage pageObject = constructorInfo.Invoke(new object[] { driver }) as Tpage;
     42  
     43             var properties = type.GetProperties(BindingFlags.Instance |
     44                 BindingFlags.Public |
     45                 BindingFlags.ExactBinding |
     46                 BindingFlags.SetProperty
     47                 );
     48  
     49             foreach (var property in properties)
     50             {
     51                 if (property.PropertyType.Name != "IWebElement" || DoesHasAttribute(property, ignoreAttributes))
     52                 {
     53                     //Do not init webelement that marked as Ignore
     54                     continue;
     55                 }
     56                 //When fetchAttributes==null, all property without ignore attributes need to be set
     57                 if (fetchAttributes == null || DoesHasAttribute(property, fetchAttributes))
     58                 {
     59                     property.SetValue(
     60                         pageObject,
     61                         driver.FindElement(
     62                             ParseUIMaps(type.Name,
     63                                 property.Name)),
     64                         null);
     65                 }
     66             }
     67             return pageObject;
     68         }
     69  
     70         public static Tpage InitPage<Tpage>(IWebDriver driver) where Tpage : class
     71         {
     72             return ConstructPage<Tpage>(driver, new List<Type>() { typeof(IgnoreAttribute) }, null);
     73         }
     74  
     75         public static Tpage RefreshPage<Tpage>(IWebDriver driver) where Tpage : class
     76         {
     77             return ConstructPage<Tpage>(driver, new List<Type>() { typeof(IgnoreAttribute) }, new List<Type>() { typeof(NeedRefreshAttribute) });
     78         }
     79  
     80         private static By ParseUIMaps(string pageName, string uimapId)
     81         {
     82             XmlSerializer serializer = new XmlSerializer(typeof(Map));
     83             Map map = null;
     84             using (FileStream fileStream = new FileStream(Path.Combine(UIMapFilePath, pageName + ".xml"), FileMode.Open))
     85             {
     86                 map = serializer.Deserialize(fileStream) as Map;
     87             }
     88             if (map == null)
     89             {
     90                 throw new Exception("Fail to deserialize UIMap xml file!!");
     91             }
     92  
     93             var uimap = (from i in map.UIMaps where i.Id == uimapId select i).Single();
     94             return ConstructBy(uimap);
     95         }
     96  
     97         private static By ConstructBy(UIMap uimap)
     98         {
     99             By by = null;
    100             switch (uimap.By)
    101             {
    102                 case "ClassName":
    103                     return By.ClassName(uimap.ToFind);
    104                 case "CssSelector":
    105                     return By.CssSelector(uimap.ToFind);
    106                 case "Id":
    107                     return By.Id(uimap.ToFind);
    108                 case "LinkText":
    109                     return By.LinkText(uimap.ToFind);
    110                 case "Name":
    111                     return By.Name(uimap.ToFind);
    112                 case "PartialLinkText":
    113                     return By.PartialLinkText(uimap.ToFind);
    114                 case "TagName":
    115                     return By.TagName(uimap.ToFind);
    116                 case "XPath":
    117                     return By.XPath(uimap.ToFind);
    118             }
    119             return null;
    120         }
    121     }
    122 }

    上面的代码通过使用反射技术,读取与Page类的名字相同的xml文件,并从中取得信息来实例化Page类的对象。
    再实现一个扩展方法,简化代码:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using Selenium.Tools;
     6 using OpenQA.Selenium;
     7  
     8 namespace WebTest.TestFramework
     9 {
    10     public static class UIMapperHelper
    11     {
    12         public static Tpage Refresh<Tpage>(this Tpage page, IWebDriver driver) where Tpage : class
    13         {
    14             return PageFactory.RefreshPage<Tpage>(driver);
    15         }
    16  
    17         public static Tpage Init<Tpage>(this Tpage page, IWebDriver driver) where Tpage : class
    18         {
    19             return PageFactory.InitPage<Tpage>(driver);
    20         }
    21     }
    22 }

    好了,testcase的代码如下(使用Nunit实现):

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5  
     6 using OpenQA.Selenium;
     7 using OpenQA.Selenium;
     8 using OpenQA.Selenium.Firefox;
     9 using OpenQA.Selenium.IE;
    10 using OpenQA.Selenium.Support.UI;
    11 using OpenQA.Selenium.Interactions;
    12 using OpenQA.Selenium.Remote;
    13 using OpenQA.Selenium.Support.PageObjects;
    14  
    15 using NUnit.Framework;
    16 using WebTest.TestFramework;
    17 using PageObjectFactory = Selenium.Tools.PageFactory;
    18  
    19 namespace WebTest.TestCases
    20 {
    21     [TestFixture]
    22     public class BVT : WebTestBase
    23     {
    24         [Test]
    25         [TestCase("camel", "123456")]
    26         public void LoginTest(string username, string passwd)
    27         {
    28             IWebDriver driver = new FirefoxDriver();
    29             driver.Navigate().GoToUrl("http://172.16.1.123:8080");
    30  
    31             PageObjectFactory.UIMapFilePath = @"E:\src_test\WebTest\TestFramework\UIMaps";
    32             LoginPage loginPage = PageObjectFactory.InitPage<LoginPage>(driver);
    33             loginPage.SelectLanguage(LanguageType.English);
    34  
    35             loginPage = loginPage.Refresh(driver);
    36             loginPage.Login(username, passwd);
    37  
    38             this.AddTestCleanup("Close Browser",
    39                 () => { driver.Close(); });
    40         }
    41     }
    42 }

    其中,”E:\src_test\WebTest\TestFramework\UIMaps”目录包含的是描述LoginPage页面的LoginPage.xml文件。
    好了,以后如果LoginPage的UI有变化时,只需要修改LoginPage.xml文件,不会影响到测试代码。

     

     

     

     

     

     

     

  • 相关阅读:
    VS2013 自动添加头部注释 -C#开发
    在调用Response.End()时,会执行Thread.CurrentThread.Abort()操作
    React
    WebApi基础
    wcf
    memcached系列
    Ioc容器Autofac系列
    使用TortoiseSVN创建版本库
    使用libcurl 发送post请求
    值得推荐的C/C++框架和库
  • 原文地址:https://www.cnblogs.com/liupengblog/p/2678504.html
Copyright © 2020-2023  润新知