赞助商
刚刚以SCRUM的方式结束了一个的ASP.NET网站的测试的第一个Spring,因为团队从无到有实现自动化测试系统,有必要把这次的经验和教训总结一下,以便后续的Spring可以获取一些有意义的借鉴。(文/donjuan

因为是一个技术博客,所以使用SCRUM管理这个测试项目的经验放在别的地方分享,这系列的文章分享一下使用VSTT和Selenium结合实现自动化测试系统的经验。

Selenium简介


Selenium主要是一个录制并回放的自动化测试用例编制工具,由一个录制工具Selenium IDE(一个Firefox插件,当然这个工具也可以回放啦),一个回放工具Selenium Remote Control在其他机器和其他操作系统上进行回放。Selenium的一个好处就是你可以使用它测试所有操作系统下的所有主流浏览器,至于Linux下面的konqueror和gnome下面自带的浏览器,没有试过Selenium是否支持,当然那个控制台界面下的浏览器就更没有试过啦。Selenium还有一个Selenium Grid,据说很强大,因为项目比较紧,就没有花时间去看它。

至于Selenium各个工具的用法,它的官网上有详细的文档,如果文档也没说清楚的话,那就直接读源代码吧。

Selenium和VSTT的整合

Selenium可以根据录制的步骤生成直接在NUnit中使用的C#代码,这些代码基本上都可以在VSTT中直接使用,就是一些属性需要更改。例如[TestFixture]改成[TestClass],[Test]改成[TestMethod]之类的,改好以后,启动Selenium-RC,就可以直接在VSTT里面当作普通的单元测试用例执行了。

Selenium代码优化

既然要做自动化测试,那么有一点是必须要时刻考虑的,就是在产品开发过程中,程序界面甚至是内部的类库接口也是时刻改变的。而Selenium只能记录当时录制测试用例的界面情况,因此需需要将它生成的代码分解一下,以面向对象的方式来重写。例如下面这段代码的目的是测试用户可以查看自己的博客:
  1. [TestMethod]
  2. public void TheTestTest()

  3. {

  4.     selenium.Open("/");

  5.     selenium.Click("link=登录");

  6.     selenium.WaitForPageToLoad("30000");

  7.     selenium.Type("tbUserName", "donjuan");

  8.     selenium.Type("tbPassword", "");

  9.     selenium.Click("btnLogin");

  10.     selenium.WaitForPageToLoad("30000");

  11.     selenium.Click("link=donjuan");

  12.     selenium.WaitForPageToLoad("30000");

  13.     selenium.Click("link=博客");

  14.     selenium.WaitForPageToLoad("30000");

  15. }
复制代码
但是网页页面布局,或者Html控件的Id、文本等内容随时都会被程序员修改,修改的原因有多种,例如修复新的错误(Bug),或者仅仅就是代码重构。因此作为测试团队,不能总是认为网页的内容一成不变的。而象登录这种操作,大部分测试用例都会用到,所以最好只要为登录动作创建唯一的代码 。有多个方案:

1.      为登录创建一个独立的测试用例,本来登录这个功能就是要测试的嘛,在编辑自动化测试用例列表的时候,把登录用例放在最前面。

2.      为登录动作创建一个单独的函数,例如LogOn(),然后在其他测试用例当中(包括登录的测试用例)调用这个函数,另外,因为可能会需要用到不同的 用户,所以最好把用户名和密码等变量提取出来,变成LogOn(string username, string password)之类的函数。

两个方案,显然是第二个方案的弹性大,但是对于第一个方案,如果测试人员都是新手,且对代码不熟悉的话,建议可以考虑。

于是我们的代码就变成类似下面的代码:
  1. using System;

  2. //

  3. // 这个异常是故意创建出来,用来封装所有在测试代码中发生的错误

  4. //

  5. public class CaseErrorException : Exception

  6. {

  7.     public CaseErrorException(string message)

  8.         : base()

  9.     {

  10.     }



  11.     public CaseErrorException(Exception inner)

  12.         : this(null, inner)

  13.     {

  14.     }



  15.     public CaseErrorException(string message, Exception inner)

  16.         : base(message == null ? "测试代码错误,请修复测试代码,查看InnerException属性!" :

  17.                                 string.Format("测试代码错误,请修复测试代码,详细错误信息:{0};或者查看InnerException属性!", message),

  18.               inner)

  19.     {

  20.     }

  21. }



  22. public class UserOperationsHelper

  23. {

  24.     public void LogOn(string username, string password)

  25.     {

  26.         // string.Empty留出来为测试目的服务

  27.         if (username == null)

  28.             throw new CaseErrorException(new ArgumentNullException("username"));

  29.         if (password == null)

  30.             throw new CaseErrorException(new ArgumentNullException("password"));



  31.         selenium.Open("/");

  32.         selenium.Click("link=登录");

  33.         selenium.WaitForPageToLoad(Consts.TimeToWaitForPageLoad);

  34.         selenium.Type("tbUserName", username);

  35.         selenium.Type("tbPassword", password);

  36.         selenium.Click("btnLogin");

  37.         selenium.WaitForPageToLoad(Consts.TimeToWaitForPageLoad);

  38.     }

  39. }



  40. public static class Consts

  41. {

  42.     // 将等待的时间提取成一个公开的函数,因为在今后大规模的测试

  43.     // 过程中,很多自动化测试用例不简单地执行,会导致网站响应速度

  44.     // 变慢,所以

  45.     public const string TimeToWaitForPageLoad = "30000";

  46. }



  47. public class TestLibrary

  48. {

  49.     public UserOperationsHelper UserHelper { get; private set; }

  50. }



  51. public class TestClass

  52. {

  53.     [TestMethod]

  54.     public void LogOnTest()

  55.     {

  56.         var username = "donjuan";

  57.         var password = "它是个秘密";

  58.         TestLibrary.UserHelper.LogOn(username, password);



  59.         // 在测试过程中,我们发现这个链接是

  60.         // 根据用户名而变的,为了扩展性,动态生成其标识文本

  61.         selenium.Click(string.Format("link={0}", username));

  62.         selenium.WaitForPageToLoad(Consts.TimeToWaitForPageLoad);

  63.         selenium.Click("link=博客");

  64.         selenium.WaitForPageToLoad(Consts.TimeToWaitForPageLoad);



  65.         // 执行一些必要的测试验证过程

  66.         Assert.IsTrue(selenium.IsTextPresented(...));

  67.     }

  68. }
复制代码
这里稍微解释一下,创建自动化测试代码,就是为了节省手工重复测试的工作量以及测试失误的风险。但只要是代码,都会有可能出错,因此自动化测试框架里面创建了一个CaseErrorException,这样在每次分析测试用例失败的时候,可以一眼区分开测试代码的错误和产品代码中的错误。例如在UserOperationHelper.LogOn函数中的参数检查,当然啦,在测试过程当中,有可能需要测试不输入用户名或者密码的情况下,验证登录界面是否正常工作的情况。因此在验证参数的时候,特意为这种情况留下了String.Empty的入口,而对于null值,则基本上可以判断是因为测试人员在编写代码上的失误(具体原因会在数据驱动测试里面讲到)。

至于TestLibrary的初始化,完全可以放到每一个测试类型的TestInitializer里面,如下表所示:
  1. [TestClass]
  2. public class AddBlogTest

  3. {

  4.     private TestContext testContextInstance;

  5.     public TestContext TestContext

  6.     {

  7.         get

  8.         {

  9.                 return testContextInstance;

  10.         }

  11.         set

  12.         {

  13.             testContextInstance = value;

  14.         }

  15.     }



  16.     private TestLibrary TestLibrary;

  17.     private ISelenium selenium;



  18.     [TestInitialize]

  19.     public void SetupTest()

  20.     {

  21.         TestLibrary = TestLibrary.SetupTest(TestContext);

  22.         selenium = TestLibrary.Selenium;

  23.     }



  24.     [TestCleanup]

  25.     public void TeardownTest()

  26.     {

  27.         TestLibrary.Shutdown();

  28.     }

  29. }
复制代码
咋看起来,把LogOn测试用例分解成那么多的类型,有点画蛇添足,实际上这些函数库正是为了更方便地创建后续的测试用例耗费的磨刀的功夫。例如下面的代码是基于一些创建好了的函数编写的测试用例:
  1. [TestMethod]
  2. public void CreateBlog()

  3. {

  4.     TestLibrary.UserHelper.LogOnAsAdmin();

  5.     var blog = TestLibrary.BlogHelper.CreateBlog("博客的标题", "博客的链接");



  6.     selenium.Click("link=管理博客");

  7.     selenium.WaitForPageToLoad(Consts.TimeToWaitForPageLoad);



  8.     Assert.IsTrue(selenium.IsElementPresent(string.Format("link={0}", blog.Title)));

  9. }
复制代码
未完待续……
赞助商
赞助商
TOP