• 使用WatiN进行UI自动化测试


    Watin是一个UI自动化测试工具,支持ie/firefox,官方网站:http://watin.org/

    主要有以下特点:

    • 支持主要的html元素,见:http://watin.org/documentation/element-class-mapping-table/
    • 可以通过多种属性查找html元素
    • 支持ajax站点测试
    • 支持对页面进行截图
    • 支持frames和iframe
    • 支持弹出对话框如alert, confirm, login以及模态对话框等
    • 方便的集成到你的测试工具,如:VS的单元测试,NUnit,MBUnit,Fitness等。

    如何获取

    目前最新版本为2.1,最后更新于2011(虽然好久不更新,但是用来做ui测试足够了),可以从http://sourceforge.net/projects/watin/下载,包括以下内容:

    • bin/:支持.net 2.0/3.5/4.0各版本的程序集
    • examples/:各种测试功能的简单例子
    • mozilla/:firefox浏览器插件,用于使用firefox浏览器进行测试
    • source/:完全使用C#编写的源代码
    • WatiN.chm:API文档

    它还有一个录制工具WatiN Test Recorder:http://sourceforge.net/projects/watintestrecord,也好久不更新了,目前最新的3.0 Alpha版还用不起了,稳定的版本是2.0 Beta 1228。安装在64位系统下可能没办法直接运行,还需要做以下操作:

    • 通过corflags.exe /32bit+ "Test Recorder.exe"标记为32位,corflags在vs sdk目录下。
    • 通过regsvr32 comutilities.dll注册组件

    当然,建议最好还是不要用录制工具去生成脚本,录制出来的脚本垃圾代码太多,手写测试脚本才是最可靠的。

    同类工具

    还有很多类似功能的UI测试工具:

    详细说明

    控件继承关系

    所有的控件都位于WatiN.Core命名空间下,以下仅列出部分主要类型:

    • WatiN.Core.Component
      • WatiN.Core.Element 页面上的元素都是从Element类型派生而来,提供了元素的基本属性如Id,Name,方法如Click,Focus等。
        • WatiN.Core.Element<TElement>
          • TextField 文本(<input type=hidden/>,<input type=password/>,<input type=text/>,<textarea/>)
          • Button 按钮(<button />,<input type=button />,<input type=submit />,<input type=reset />)
          • Image (<img/>, <input type=image />)
          • CheckBox
          • RadioButton
          • SelectList
          • FileUpload
          • ElementContainer<TElement> 容器类型
            • Label (<label />)
            • Link 链接(<a />)
            • Div
            • Para (<p/>)
            • Form
            • Table
            • TableBody
            • TableCell
            • TableRow

    IE类型主要方法

    整个测试都围绕IE类型的一些方法来进行,打开浏览器、查找控件、执行输入或点击操作、对结果进行校验等,那么了解它提供了哪些方法显得格外重要,这里仅列出主要的:

    • AddDialogHandler/RemoveDialogHandler:添加/移除对话框处理程序,主要用来处理alert等弹出对话框,具体见WatiN.Core.DialogHandlers命名空间下的类型
    • CaptureWebPageToFile:网页截图并保存到文件
    • WaitForComplete:等待页面加载完成
    • AttachTo:按条件在进程中查找已有的浏览器窗口,返回IE类型实例(这种方法不需要通过IE.Goto方法打开窗口)
    • RegisterAttachToHelper:注册自定义的IE类型用于AttachTo方法
    • Exists:进程中查找是否存在符合条件的浏览器窗口
    • Back/Forward/Refresh/Close/ForceClose/Reopen:后退/前进/刷新/关闭/强制关闭/关闭并重新打开空页面窗口
    • GoTo/GoToNoWait:打开URL
    • ShowWindow/SizeWindow:调整窗口大小
    • ClearCache/ClearCookies:清理缓存/清理Cookie
    • GetCookie/GetCookieContainerForUrl/GetCookiesForUrl:获取Cookie
    • SetCookie:设置Cookie

    HTML元素主要属性及方法

    这里主要列出控件基础类型Element的属性和方法

    属性,熟悉js dom的话从字面意思就能看懂:

    • Id/IdOrName/Name/ClassName/TagName/Title/Text/InnerHtml/OuterHtml/OuterText/Style 元素自身的属性
    • Parent/NextSibling/PreviousSibling/DomContainer/TextBefore/TextAfter
    • Enabled/Complete/Exists:是否启用/是否完成加载/是否存在

    方法:

    • Ancestor:查找最近的祖先元素,类似于jQuery的closest方法
    • Blur/Change/Click/ClickNoWait/DoubleClick/Focus/Flash/Highlight/KeyDown/KeyDownNoWait/KeyPress/KeyPressNoWait/KeyUp/KeyUpNoWait/MouseDown/MouseEnter/MouseUp/Refresh/FireEvent:触发控件的事件
    • GetValue(attributeName)/GetAttributeValue(attributeName):获取属性值
    • SetAttributeValue(name, value):设置属性值
    • WaitForComplete/WaitUntil/WaitUltilExists/WaitUntilRemoved:等待指定条件达成

    以上的属性、方法在支持的元素中都能使用,有一些元素还有自己单独的属性/方法,如TextField有自己的MaxLength/ReadOnly属性、TypeText/AppendText方法等。

    在页面中查找控件

    IE类型提供了诸多方法用于在页面中查找控件,其中最主要的方法如下:

      public virtual TElement ElementOfType<TElement>(string elementId) where TElement : Element; // 通过id查找
      public virtual TElement ElementOfType<TElement>(Regex elementId) where TElement : Element; // 通过正则表达式匹配id查找
      public virtual TElement ElementOfType<TElement>(Predicate<TElement> predicate) where TElement : Element; // 通过自定义方法匹配
      public virtual TElement ElementOfType<TElement>(Constraint findBy) where TElement : Element; // 通过Find类型提供的方法查找
    
      public virtual Element Element(string elementId);
      public virtual Element Element(Regex elementId);
      public virtual Element Element(Predicate<Element> predicate);
      public virtual Element Element(Constraint findBy);

    其他类型的控件一般都是由ElementOfType<TElement>方法扩展而来,如TextField:

      public virtual TextField TextField(string elementId);
      public virtual TextField TextField(Regex elementId);
      public virtual TextField TextField(Predicate<TextField> predicate);
      public virtual TextField TextField(Constraint findBy);

    这里简单演示一下TextField的使用:

      browser.TextField("lwme");
      browser.TextField(new Regex("lwme", RegexOptions.IgnoreCase));
      browser.TextField(t => t.Id.ToLowerInvariant() == "lwme");
      browser.TextField(Find.ById("lwme"));

    更灵活的使用可以直接用自定义方法匹配,或者Find类提供的方法。

    Find类提供了许多有用的方法来查找元素:

    • ById/ByName/ByClass/ByText/ByValue/ByTitle/ByUrl/BySrc/ByStyle:通过各种属性来查找元素
    • By(attributeName, …):上面的方法就是基于这个方法而定义的,通过这个方法可以查找自定义属性
    • ByIndex:按控件序号
    • ByFor/ByLabelText:按对应<label />
    • BySelector:支持jQuery/Sizzle的css Selector

    使用方法

    注:测试代码大部分来自官方例子并稍作修改。

    直接从程序集目录引用WatiN.Core.dll到项目中,由于WatiN使用了COM组件即Interop.SHDocVw.dll,所以必须使用单线程模式运行(可以使用STAThreadAttribute标识)。

    先来个简单的控制台例子:

            [STAThread]
            static void Main(string[] args)
            {
                using (var browser = new IE("http://lwme.cnblogs.com"))
                {
                    browser.TextField(Find.ById("q")).TypeText(" ");
                    browser.Image(Find.ById("btnZzk")).Click();
                    Console.WriteLine(browser.ContainsText("囧月"));
                }
                Console.Read();
            }

    在Visual Studio单元测试中运行

    在使用vs单元测试中一般会用到以下Attribute:

    • AssemblyInitialize/AssemblyCleanup:程序集加载之后/程序集卸载之前
    • ClassInitialize/ClassCleanup:类加载之后/类卸载之前
    • TestInitialize/TestCleanup:每个测试方法运行之前/之后
    • TestClass:每个测试的类都必须有这个属性
    • TestMethod:每个测试的方法都必须有这个属性

    在测试过程中还会用到各种Assert类型来对结果进行校验,更多参考:http://msdn.microsoft.com/zh-cn/library/ms243147(v=vs.80).aspx#中国(简体中文)

    先来个简单的Google搜索测试:

    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using WatiN.Core;
    
    namespace TestProject
    {
        [TestClass]
        public class GoogleTests
        {
            [TestMethod, STAThread]
            public void Search_for_watin_on_google_the_old_way()
            {
                using (var browser = new IE("http://www.google.com.hk"))
                {
                    browser.TextField(Find.ByName("q")).TypeText("WatiN");
                    browser.Button(Find.ByName("btnK")).Click();
                    Assert.IsTrue(browser.ContainsText("WatiN"));
                }
            }
        }
    }

    以上是老版本的测试代码,在新版本中还支持一种自定义的Page,把HTML元素作为Page的字段并用FindByAttribute进行标识,可以最大程度做到代码重用:

    [Page(UrlRegex = "www.google.*")]
    public class GoogleSearchPage : Page
    {
        [FindBy(Name = "q")] 
        public TextField SearchCriteria;
    
        [FindBy(Name = "btnK")] 
        public Button SearchButton;
    }

    现在,测试代码变成了:

            [TestMethod, STAThread]
            public void Search_for_watin_on_google_using_page_class()
            {
                using (var browser = new IE("http://www.google.com.hk"))
                {
                    var searchPage = browser.Page<GoogleSearchPage>();
                    searchPage.SearchCriteria.TypeText("WatiN");
                    searchPage.SearchButton.Click();
                    Assert.IsTrue(browser.ContainsText("WatiN"));
                }
            }

    还可以更进一步的达到代码重用:

            [TestMethod, STAThread]
            public void Page_with_an_action()
            {
                using (var browser = new IE("http://www.google.com.hk"))
                {
                    browser.Page<GoogleSearchPage>().SearchFor("WatiN");
                    Assert.IsTrue(browser.ContainsText("WatiN"));
                }
            }
    
            [Page(UrlRegex = "www.google.*")]
            public class GoogleSearchPage : Page
            {
                [FindBy(Name = "q")] 
                public TextField SearchCriteria;
    
                [FindBy(Name = "btnK")] 
                public Button SearchButton;
    
                public void SearchFor(string searchCriteria)
                {
                    SearchCriteria.TypeText("WatiN");
                    SearchButton.Click();
                }
            }

    不过可惜的是FindByAttribute不支持自定义属性,所以,在需要用到自定义属性的时候就不能用FindByAttribute,而要改用Find类型提供的方法:

    [Page(UrlRegex = "www.google.*")]
    public class GoogleSearchPage : Page
    {
        public TextField SearchCriteria
        {
            get { return Document.TextField(Find.ByName("q")); }
        }
    
        public Button SearchButton
        {
            get { return Document.Button(Find.ByName("btnK")); }
        }
    
        public void SearchFor(string searchCriteria)
        {
            SearchCriteria.TypeText("WatiN");
            SearchButton.Click();
        }
    }

    从已有的窗口返回IE实例

    主要使用AttachTo方法,查找已经打开的窗口返回IE实例:

            [TestMethod, STAThread]
            public void Attach_should_return_MyIE_instance()
            {
                new IE("www.google.com.hk") { AutoClose = false };
                var myIe = Browser.AttachTo<IE>(Find.ByTitle("Google"));
                Assert.IsNotNull(myIe);
                Assert.IsTrue(myIe.Title.StartsWith("Google"));
                myIe.Close();
            }

    还可以自定义IE类型:

        public class MyIE : IE
        {
            public MyIE(string url) : base(url) { }
            public MyIE(IEBrowser browser) : base(browser) { }
            public string MyDescription
            {
                get
                {
                    return Title + " opened by 囧月 " + Url;
                }
            }
        }
        public class AttachToMyIEHelper : AttachToIeHelper
        {
            protected override IE CreateBrowserInstance(IEBrowser browser)
            {
                return new MyIE(browser);
            }
        }

    然后通过注册AttachHelper来返回自定义IE实例:

        [TestClass]
        public class MyIEAttachToHelperExample
        {
            static MyIEAttachToHelperExample()
            {
                Browser.RegisterAttachToHelper(typeof(MyIE), new AttachToMyIEHelper());
            }
    
            [TestMethod, STAThread]
            public void Attach_should_return_MyIE_instance()
            {
                new IE("www.google.com.hk") { AutoClose = false };
                var myIe = Browser.AttachTo<MyIE>(Find.ByTitle("Google"));
                Assert.IsNotNull(myIe);
                Assert.IsTrue(myIe.MyDescription.StartsWith("Google"));
                Assert.IsTrue(myIe.MyDescription.Contains("囧月"));
                Assert.IsTrue(myIe.MyDescription.EndsWith(myIe.Url));
                myIe.Close();
            }
        }

    共享同一个IE实例

    很多时候想要置创建一个IE实例,然后扎起多个测试方法中共享IE实例,那么就很可能有这种代码:

        [TestClass]
        public class ProblemWithSharingTests
        {
            private static IE ie;
    
            [ClassInitialize]
            public static void testInit(TestContext testContext)
            {
                ie = new IE("http://lwme.cnblogs.com");
            }
    
            [TestMethod]
            public void testOne()
            {
                Assert.IsTrue(ie.ContainsText("囧月"));
            }
    
            [TestMethod]
            public void testTwo()
            {
                Assert.IsTrue(ie.ContainsText("囧月"));
            }
        }

    但是在运行里面会发现其中有一个测试会运行失败,在官方的例子中给出了一个解决方法,先定义如下类型:

        public class IEStaticInstanceHelper
        {
            private IE _ie;
            private int _ieThread;
            private string _ieHwnd;
    
            public IEStaticInstanceHelper()
            {
                Console.WriteLine("created");
            }
            public IE IE
            {
                get
                {
                    var currentThreadId = GetCurrentThreadId();
                    Console.WriteLine(currentThreadId + ", was:" + _ieThread);
                    if (currentThreadId != _ieThread)
                    {
                        _ie = IE.AttachTo<IE>(Find.By("hwnd", _ieHwnd));
                        _ieThread = currentThreadId;
                    }
                    return _ie;
                }
                set
                {
                    _ie = value;
                    _ieHwnd = _ie.hWnd.ToString();
                    _ieThread = GetCurrentThreadId();                   
                }
            }
    
            private int GetCurrentThreadId()
            {
                return Thread.CurrentThread.ManagedThreadId;
            }
        }

    每次在获取IE实例的时候判断线程ID是不是当前线程ID,如果不是则通过AttachTo方法获取已有窗口再返回,从而解决了由于共享IE实例导致测试失败的错误。

    新的测试代码如下:

        [TestClass]
        public class UnitTest 
        {
            private static IEStaticInstanceHelper ieStaticInstanceHelper;
            private static int _ieThread;
    
            [ClassInitialize]
            [STAThread]
            public static void testInit(TestContext testContext)
            {
                ieStaticInstanceHelper = new IEStaticInstanceHelper();
                Settings.AutoStartDialogWatcher = false;
                ieStaticInstanceHelper.IE = new IE("http://lwme.cnblogs.com");
                _ieThread = Thread.CurrentThread.ManagedThreadId;
            }
    
            public IE IE
            {
                get { return ieStaticInstanceHelper.IE; }
                set { ieStaticInstanceHelper.IE = value; }
            }
    
            [ClassCleanup]
            [STAThread]
            public static void MyClassCleanup()
            {
                ieStaticInstanceHelper.IE.Close();
                ieStaticInstanceHelper = null;
            }
    
            [TestMethod]
            [STAThread]
            public void testOne()
            {
                lock (this)
                {
                    Assert.AreEqual(_ieThread, Thread.CurrentThread.ManagedThreadId);
                    Assert.IsTrue(IE.ContainsText("囧月"));
                }
            }
    
            [TestMethod]
            [STAThread]
            public void testTwo()
            {
                lock (this)
                {
                    Assert.AreNotEqual(_ieThread, Thread.CurrentThread.ManagedThreadId);
                    Assert.IsTrue(IE.ContainsText("囧月"));
                }
            }
            [TestMethod]
            [STAThread]
            public void testThree()
            {
                lock (this)
                {
                    Assert.AreNotEqual(_ieThread, Thread.CurrentThread.ManagedThreadId);
                    Assert.IsTrue(IE.ContainsText("囧月"));
                }
            }
        }

    运行javascript

    browser或者html元素的DomContainer都有Eval/RunScript方法用以运行脚本,其中Eval可以获取从js返回的值。

            [TestMethod, STAThread]
            public void test_javascript()
            {
                using (var browser = new IE("http://www.google.com.hk/"))
                {
                    var now = DateTime.Now;
                    var q = browser.TextField(Find.ByName("q"));
                    var jsobjref = "document.querySelector('input[name=q]')";
                    Assert.IsTrue(string.IsNullOrEmpty(browser.Eval(jsobjref + ".value")));
                    browser.RunScript(jsobjref + ".value='" + now.ToShortDateString() + "';");
                    Assert.AreEqual(now.ToShortDateString(), browser.Eval(jsobjref + ".value"));
                    browser.RunScript(jsobjref + ".value='囧月';");
                    Assert.AreEqual("囧月", browser.Eval(jsobjref + ".value"));
                }
            }

    对于ajax的测试也是依赖这两个方法。

    弹出对话框

    假如存在以下的服务端代码用于登录:

    protected void doLogin_click(object sender, EventArgs e)
    {
      if (username.Text == "lwme" && password.Text == "lwme")
      {
         ClientScript.RegisterStartupScript(this.GetType(), "login", "alert('登录成功');", true);
      }
      else
      {
        ClientScript.RegisterStartupScript(this.GetType(), "login", "alert('登录失败');", true);
      }
    }

    那么就可以这样测试登录逻辑:

            [TestMethod, STAThread]
            public void Test_Login_success_with_dialog()
            {
                using (IE ie = new IE("localhost/login.aspx"))
                {
                    AlertDialogHandler adh = new AlertDialogHandler();
                    ie.AddDialogHandler(adh);
                    ie.TextField("username").TypeText("lwme");
                    ie.TextField("password").TypeText("lwme");
                    ie.Button("doLogin").Click();
                    adh.WaitUntilExists();
                    string msg = adh.Message;
                    adh.OKButton.Click();
                    ie.WaitForComplete();
                    ie.RemoveDialogHandler(adh);
                    Assert.IsTrue(msg.Contains("登录成功!"));
                }
            }
    
            [TestMethod, STAThread]
            public void Test_Login_failed_with_dialog()
            {            
                using (IE ie = new IE("localhost/login.aspx"))
                {
                    AlertDialogHandler adh = new AlertDialogHandler();
                    ie.AddDialogHandler(adh);
                    ie.TextField("username").TypeText("test");
                    ie.TextField("password").TypeText("test");
                    ie.Button("doLogin").Click();
                    adh.WaitUntilExists();
                    string msg = adh.Message;
                    adh.OKButton.Click();
                    ie.WaitForComplete();
                    ie.RemoveDialogHandler(adh);
                    Assert.IsTrue(msg.Contains("登录失败"));
                }
            }

    URL跳转

    假如登录之后进行url跳转:

      if (username.Text == "admin" && password.Text == "admin")
      {
         Response.Redirect("index.aspx");
      }

    那么可以这样去测试逻辑:

            [TestMethod, STAThread]
            public void Test_Login_success_with_redirect()
            {
                using (IE ie = new IE("localhost/login.aspx"))
                {
                    ie.TextField("username").TypeText("lwme");
                    ie.TextField("password").TypeText("lwme");
                    ie.Button("doLogin").ClickNoWait();
                    ie.WaitForComplete();
                    Assert.IsTrue(ie.Url.EndsWith("index.aspx", StringComparison.InvariantCultureIgnoreCase));
                }
            }

    结尾

    本文只是对WatiN功能简单的做一些介绍,更多有用的功能还有待挖掘。

    话说WatiN已经好久不更新了,目前看来Visual Studio 的Coded UI Test或许是一个不错的选择。

    --EOF--

  • 相关阅读:
    [转]Xml Schema
    设计模式之Observer Pattern
    通过 C# 使用 J# 类库中的 Zip 类压缩文件
    An extender can't be in a different UpdatePanel than the control it extends
    关于AutoResetEvent和ManualResetEvent
    ref, out参数区别
    取整, 无条件进位, 无条件取整
    VB.NET语法基础
    XP防火墙,挡掉访问自己的IIS
    maybe useful for Add the solution to source control
  • 原文地址:https://www.cnblogs.com/lwme/p/automated-ui-testing-with-watin.html
Copyright © 2020-2023  润新知