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文件,不会影响到测试代码。

 

 

 

 

 

 

 

原文地址:https://www.cnblogs.com/liupengblog/p/2678504.html