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测试工具:
- Selenium:官网http://www.seleniumhq.org/,它是一个非常强大的UI自动化测试工具,支持Java/C#/Ruby/Python/php/perl/NodeJS等语言,通过Selenium WebDriver可以支持ie/firefox/chrome/safari甚至包括iphone/android等浏览器,还可以通过firefox插件Selenium IDE来进行脚本录制。
- Watir:官网http://watir.com/,它是ruby的库。只支持windows下的IE,但是通过Watir-WebDriver支持chrome/firefox/ie等浏览器。
- WatiJ:官网http://watij.com/,支持ie/firefox/safari,可以通过java/jruby编写测试脚本。
- Sahi:官网http://sahi.co.in/,含收费的Pro版和免费的OpenSource版,对web 2.0应用测试支持比较好,可以通过js编写脚本。
- Coded UI Test:从Visual Studio 2010开始出现的,跟VS高度集成(Premium/Ultimate版才支持),功能相当强大。具体见:http://msdn.microsoft.com/zh-cn/library/dd286726(v=vs.120).aspx#中国(简体中文)
详细说明
控件继承关系
所有的控件都位于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类型提供了诸多方法用于在页面中查找控件,其中最主要的方法如下:
1
2
3
4
5
6
7
8
9
|
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:
1
2
3
4
|
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的使用:
1
2
3
4
|
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标识)。
先来个简单的控制台例子:
1
2
3
4
5
6
7
8
9
10
11
|
[STAThread]
static
void
Main(
string
[] args)
{
{
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搜索测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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()
{
{
browser.TextField(Find.ByName(
"q"
)).TypeText(
"WatiN"
);
browser.Button(Find.ByName(
"btnK"
)).Click();
Assert.IsTrue(browser.ContainsText(
"WatiN"
));
}
}
}
}
|
以上是老版本的测试代码,在新版本中还支持一种自定义的Page,把HTML元素作为Page的字段并用FindByAttribute进行标识,可以最大程度做到代码重用:
1
2
3
4
5
6
7
8
9
|
[Page(UrlRegex =
"www.google.*"
)]
public
class
GoogleSearchPage : Page
{
[FindBy(Name =
"q"
)]
public
TextField SearchCriteria;
[FindBy(Name =
"btnK"
)]
public
Button SearchButton;
}
|
现在,测试代码变成了:
1
2
3
4
5
6
7
8
9
10
11
|
[TestMethod, STAThread]
public
void
Search_for_watin_on_google_using_page_class()
{
{
var
searchPage = browser.Page<GoogleSearchPage>();
searchPage.SearchCriteria.TypeText(
"WatiN"
);
searchPage.SearchButton.Click();
Assert.IsTrue(browser.ContainsText(
"WatiN"
));
}
}
|
还可以更进一步的达到代码重用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
[TestMethod, STAThread]
public
void
Page_with_an_action()
{
{
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类型提供的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
[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实例:
1
2
3
4
5
6
7
8
9
|
[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类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
[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实例,那么就很可能有这种代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
[TestClass]
public
class
ProblemWithSharingTests
{
private
static
IE ie;
[ClassInitialize]
public
static
void
testInit(TestContext testContext)
{
}
[TestMethod]
public
void
testOne()
{
Assert.IsTrue(ie.ContainsText(
"囧月"
));
}
[TestMethod]
public
void
testTwo()
{
Assert.IsTrue(ie.ContainsText(
"囧月"
));
}
}
|
但是在运行里面会发现其中有一个测试会运行失败,在官方的例子中给出了一个解决方法,先定义如下类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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实例导致测试失败的错误。
新的测试代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
[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
;
_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返回的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
[TestMethod, STAThread]
public
void
test_javascript()
{
{
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的测试也是依赖这两个方法。
弹出对话框
假如存在以下的服务端代码用于登录:
1
2
3
4
5
6
7
8
9
10
11
|
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
);
}
}
|
那么就可以这样测试登录逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
[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跳转:
1
2
3
4
|
if
(username.Text ==
"admin"
&& password.Text ==
"admin"
)
{
Response.Redirect(
"index.aspx"
);
}
|
那么可以这样去测试逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
|
[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--