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

 

 

 

 

 

 

 

你可能感兴趣的:(selenium)