[小北De编程手记] [Lesson 02] AutoFramework构建 之 Page Objects - 设计模式
写在最前面
这个系列的主旨是要跟大家分享一下关于自动化测试框架的构建的一些心得。这几年,做了一些自动化测试框架以及团队的构建的工作。过程中遇到了很多这样的同学,他们在学习了某一门语言和一些自动化测试的类库或者工具之后,打算进一步的提高。我想这个系列也许会帮助到你,我们一起从各个维度来看一看自动化测试框架的一些最佳实践。本人能力有限,如果有什么不正确的的地方还各位大牛指正。
自动化测试 - 设计模式
设计模式(Design pattern)代表了最佳的实践,是针对一些特定场景下问题的解决方案。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。这是网络上比较通用的对设计模式的解释。
那么,对于自动化测试架构来说又有哪些最佳实践呢?
- 领域驱动设计(Domain Driven Design):用接近终端用户的语言来表达你的测试。
- 页面对象(Page Objects):基于UI的简单抽象。
- 组件化(Loadable Component):组件化项目中的待测试元素。
- 基于命令的测试(Bot Style Tests):基于命令而不是对象的设计,对Page Objects的补充
- 验收测试(Acceptance Tests):使用粗粒度的UI测试来帮助结构开发工作。
- 回归测试(Regression Tests):将多个验收测试的操作收集到一个地方以方便维护。
注意:这里的“验收测试”和“回归测试”是指对自动化测试用例的组织方式,而不是通常意义上的测试过程。虽然这也会成为实际测试活动的一部分。
这个系列重要内容之一,就是为大家一一讲解上面的模式。 因此,这一个小节我会放在之后所有关于自动化设计模式问文章开头,因为他真的很重要~~~ 关于上述模式的总结,你在也可以在Selenium项目的Wiki里找到,链接如下: https://github.com/SeleniumHQ/selenium/wiki/Design-Patterns
PageObject模式目的
这一篇,我们要讨论的是上述模式中最有名,也是最被误用最多的模式:Page Objects。简而言之,Page Objects是我们针对页面以及页面相关服务的封装。对于使用者来说,每一个封装好的页面对象都为他们提供了获取页面数据和页面相关的操作方法。让使用者可以简单的像手工操作一样的来书写自动化测试的用例。
我们来回顾一下上一讲中的那个例子:
1 namespace AutoFramework.TestCase 2 { 3 public class TestSuite_Demo : TestBase 4 { 5 public TestSuite_Demo(TestContextFixture context, ServiceFixture service, DBFixture database) 6 : base(context, service, database) { } 7 8 [Fact(DisplayName = "TestCase.Demo001")] 9 public void Demo_Case_Create() 10 { 11 //-->Data preparation. 12 var userModel = ModelBuilder.ForJsonFile<UserModel>("DemoCase/TestData.json"); 13 14 //-->Test case exec way 01. 15 var signInPage = Router.GoTo<SignInPage>(); 16 var homePage = signInPage.SignIn("user-name", "pwd"); 17 var addUserPage = homePage.NavMenu.Select<AddUserPage>("User", "New"); 18 var userListPage = addUserPage.AddUser(userModel); 19 20 //-->Test case exec way 02. 21 /*You can custom this 'Workflow'*/ 22 var userListPage = Router.GoTo<SignInPage>() 23 .SignIn("user-name", "pwd") 24 .NavMenu.Select<AddUserPage>("User", "New") 25 .AddUser(userModel); 26 27 Assert.True(userListPage.IsExistUser(userModel.Name)); 28 } 29 } 30 }
其中,signInPage,homePage,addUserPage,userListPage 都是一些页面对象。对于书写Test Case 的人而言,他们可以不需要过多的关心页面上的实现细节。而是,更加关注业务本身。这也是框架设计的目的之一:分层
自动化测试目的主要是模拟手工操作,而很多测试框架比如Selenium,Appnium,white... ...等都提供了基本的驱动支持。在实际自动化构建测试构建过程中,UI改变导致的测试用例运行失败应该是自动化需要解决的首要问题,而PageObjects的好处之一,就是可以较好应对UI的改变对测试用维护成本的影响。
PageObject模式原则
上一个小节,我们看到了如何使用封装好的PageObject对象。那么,应该如何来设计(封装)一个PageObject呢?下面列出了一个些PageObject设计的原则:
- 提供公共方法来表示页面提供的服务
- 尽量不要暴露页面的内部细节
- 一般不要在Page Objects中使用断言(CheckPoint),因为检查点往往是测试用例应当关心的。
- 提供页面操作的方法返回其他PageObjects
- Page Objects不需要完整的表达整个页面,也可以是一个组件。
- 相同操作的如果结果不同,应当创建不同的方法。
还是以登录页面为例:
1 public class SignInPage : PageObjectBase 2 { 3 public SignInPage(IWebDriver driver) : base(driver) 4 { 5 WaitSelector.WaitingFor_ElementExists(this.Driver,By.Id("ContentPlaceHolder1_txtUsername")); 6 7 txtUserName = new TextBox(driver, By.Id("txtUsername")); 8 txtPassword = new TextBox(driver, By.Id("txtPassword")); 9 btnSignIn = new Button(driver, By.XPath(".//input[@value='Sign In']")); 10 } 11 12 #region Page elements 13 protected TextBox txtUserName; 14 protected TextBox txtPassword; 15 protected Button btnSignIn; 16 #endregion Page elements 17 18 #region Action for test case 19 /// <summary> 20 /// Sign In 21 /// </summary> 22 /// <param name="userName">User name</param> 23 /// <param name="password">Password</param> 24 public HomePage SignIn(string userName, string password) 25 { 26 this.txtUserName.Clear(); 27 this.txtPassword.Clear(); 28 29 this.txtUserName.SendKeys(userName); 30 this.txtPassword.SendKeys(password); 31 this.btnSignIn.Click(); 32 33 return new HomePage(this.Driver); 34 } 35 36 public SignInPage SignInError(string userName, string password) 37 { 38 this.txtUserName.Clear(); 39 this.txtPassword.Clear(); 40 41 this.txtUserName.SendKeys(userName); 42 this.txtPassword.SendKeys(password); 43 this.btnSignIn.Click(); 44 45 return new this(this.Driver); 46 } 47 #endregion 48 }
可以看到,对于登录页面的实现。基本的页面操作(比如登录),如果行为是不同的建议提供两个不同的方法。登录成功返回的是HomePage。但是如果失败了返回的是当前页面。这样的编码方式既清晰,也同时满足了前面例子中的链式调用的简洁。
在方法的内部,我们需要处理页面的等待延时处理,元素查找等一系列与UI相关的操作。所谓Page Objects的封装,就是希望使用Page Objects的人不需要关心页面上的细节。
关于检查点,很多实现是在Page Objects内部提供了一些方法来检查,例如:
1 public SignInPage CheckErrorMessage(string message) 2 { 3 //... ... 4 Assert.True(message,"xxxx"); 5 6 return new this(this.Driver); 7 }
我们来思考一下,断言检查也就是检查Check Point应该由是测试用例本身负责,还是由提供页面服务的Page Objects来负责呢?很明显我们不应该在Page Objects内部处理测试用例的检查点。下面的这种处理方式是Page Objects模式本身建议的更好的做法:
让我们以下面的这个UI为例:
1 public string GetErrorMessage() 2 { 3 //返回页面上的错误信息 4 } 5 6 //调用代码 7 ... 8 var signInPage = Router.GoTo<SignInPage>(); 9 var errorMsg = signInPage.SignIn("user-name", "pwd").GetErrorMessage(); 10 Assert.Equal(errorMsg,"用户名密码错误!");
设计模式的使用都有它的场景的意图。Page Objects模式的设计意图主要就是为了解决一下几个问题:
- 重用操作
- 使得自动化测试用例的编写不用关心过多的UI相关的细节
- 应对UI的变化
细心的同学也许已经注意到了,UI的操作我们并没有直接是用Selenium的WebElement,而是做了一些封装。使用了类似Button,TextBox 这样的对象。这一部分涉及到的是我们后面要讲到的另一个设计模式Loadable Component 和 Bot Style Tests, 后面的课程会对这个专门进行讲解。
这一篇就先到这里啦~~,自动化测试框架的构建是需要一定知识基础和自动化测试经验的。希望我们能在接下来的这段时间里一起提高,但愿我的观点对你有所帮助~~~
小北De系列文章:
《[小北De编程手记] : Selenium For C# 教程》
《[小北De编程手记]:玩转 xUnit.Net》(未完成)