Watin是一个UI自动化测试工具,支持ie/firefox,官方网站:http://watin.org/。
主要有以下特点:
目前最新版本为2.1,最后更新于2011(虽然好久不更新,但是用来做ui测试足够了),可以从http://sourceforge.net/projects/watin/下载,包括以下内容:
它还有一个录制工具WatiN Test Recorder:http://sourceforge.net/projects/watintestrecord,也好久不更新了,目前最新的3.0 Alpha版还用不起了,稳定的版本是2.0 Beta 1228。安装在64位系统下可能没办法直接运行,还需要做以下操作:
当然,建议最好还是不要用录制工具去生成脚本,录制出来的脚本垃圾代码太多,手写测试脚本才是最可靠的。
还有很多类似功能的UI测试工具:
所有的控件都位于WatiN.Core命名空间下,以下仅列出部分主要类型:
整个测试都围绕IE类型的一些方法来进行,打开浏览器、查找控件、执行输入或点击操作、对结果进行校验等,那么了解它提供了哪些方法显得格外重要,这里仅列出主要的:
这里主要列出控件基础类型Element的属性和方法
属性,熟悉js dom的话从字面意思就能看懂:
方法:
以上的属性、方法在支持的元素中都能使用,有一些元素还有自己单独的属性/方法,如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类提供了许多有用的方法来查找元素:
注:测试代码大部分来自官方例子并稍作修改。
直接从程序集目录引用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();
        }
在使用vs单元测试中一般会用到以下Attribute:
在测试过程中还会用到各种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();
    }
}
主要使用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实例,那么就很可能有这种代码:
    [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("囧月"));
            }
        }
    }
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跳转:
  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--