使用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--


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM