Introduction
WatiN is a great .NET library for writing automated browser based tests that uses real browser to go to websites, perform actions and check for browser output. Combined with a unit test library like xUnit, you can use WatiN to perform automated regression tests on your websites and save many hours of manual testing every release. Moreover, WatiN can be used to stress test JavaScripts on the page as it can push the browser to perform operations repeatedly and measure how long it takes for Javascripts to run. Thus you can test your Javascripts for performance, rendering speed of your website and ensure the overall presentation is fast and smooth for users.
I have written some extension methods for WatiN to help facilitate AJAX related tests, especially with ASP.NET UpdatePanel
, jQuery and dealing with element positions. I will show you how to use these libraries to test sophisticated AJAX websites, like the one I have built - Dropthings, which is a widget powered ASP.NET AJAX portal using ASP.NET UpdatePanel
, jQuery to create a Web 2.0 presentation. You can simulate and test UpdatePanel
updates, AJAX calls and UI update and even drag & drop of widgets!
You can see the implementation of automated tests in my open source project codebase.
WatiN Extension for ASP.NET AJAX and jQuery
When you are testing an ASP.NET AJAX website that uses the UpdatePanel
, you need to make sure async postbacks are successfully completed before testing the page output for correctness. For example, you clicked a button using WatiN and that causes an UpdatePanel
to update the page. You need to wait for the async postback to complete before you can test for the correct output. Here’s a code snippet that does it:
internal static bool WaitForAsyncPostbackComplete(this Browser browser, int timeout)
{
int timeWaitedInMilliseconds = 0;
var maxWaitTimeInMilliseconds = Settings.WaitForCompleteTimeOut * 1000;
var scriptToCheck =
"Sys.WebForms.PageRequestManager.getInstance().get_isInAsyncPostBack();";
while (bool.Parse(browser.Eval(scriptToCheck)) == true
&& timeWaitedInMilliseconds < maxWaitTimeInMilliseconds)
{
Thread.Sleep(Settings.SleepTime);
timeWaitedInMilliseconds += Settings.SleepTime;
}
return bool.Parse(browser.Eval(scriptToCheck));
}
Similarly for jQuery calls, if you are making AJAX calls using the jQuery’s default implementation, then you need to ensure the Ajax calls completed before you test for any UI update. The way to do it is first call this right after your page is loaded so that it installs a hook to monitor Ajax calls:
internal static void InjectJQueryAjaxMonitor(this Browser browser)
{
const string monitorScript =
@"function AjaxMonitor(){"
+ "var ajaxRequestCount = 0;"
+ "$(document).ajaxStart(function(){"
+ " ajaxRequestCount++;"
+ "});"
+ "$(document).ajaxComplete(function(){"
+ " ajaxRequestCount--;"
+ "});"
+ "this.isRequestInProgress = function(){"
+ " return (ajaxRequestCount > 0);"
+ "};"
+ "}"
+ "var watinAjaxMonitor = new AjaxMonitor();";
browser.Eval(monitorScript);
}
Then use this to wait for Ajax calls to complete:
internal static void WaitForJQueryAjaxRequest(this Browser browser)
{
int timeWaitedInMilliseconds = 0;
var maxWaitTimeInMilliseconds = Settings.WaitForCompleteTimeOut * 1000;
while (browser.IsJQueryAjaxRequestInProgress()
&& timeWaitedInMilliseconds < maxWaitTimeInMilliseconds)
{
Thread.Sleep(Settings.SleepTime);
timeWaitedInMilliseconds += Settings.SleepTime;
}
}
internal static bool IsJQueryAjaxRequestInProgress(this Browser browser)
{
var evalResult = browser.Eval("watinAjaxMonitor.isRequestInProgress()");
return evalResult == "true";
}
Sometimes you have to simulate a user visit for the first time. So, you need clear cache and cookie before you hit the site. Here’s how to do it.
private static void ClearCookiesInIE()
{
Process.Start("RunDll32.exe", "InetCpl.cpl,ClearMyTracksByProcess 2").WaitForExit();
}
private static void DeleteEverythingInIE()
{
Process.Start("RunDll32.exe", "InetCpl.cpl,ClearMyTracksByProcess 255").WaitForExit();
}
Sometimes you need to know the position of elements on the page so that you can simulate mouse clicks on a certain location on the page. Here’s a snippet that returns the position of the element on the browser.
internal static int[] FindPosition(this Browser browser, Element e)
{
var top = 0;
var left = 0;
var item = e;
while (item != null)
{
top += int.Parse(item.GetAttributeValue("offsetTop"));
left += int.Parse(item.GetAttributeValue("offsetLeft"));
item = item.Parent;
}
return new int [] { left, top };
}
Now you are ready to write some sophisticated AJAX tests. I will show you some common scenarios like showing content from async update inside UpdatePanel
and then verifying the necessary UI elements are there, then adding new items to the page dynamically and checking if they get added properly and even some drag & drop simulated by WatiN.
Creating Page Adapters using WatiN
WatiN has a useful concept called Page Adapter. You can create a class that represents functionalities on the page. For example, the class can expose properties which represent important elements on the page. It can offer functions to perform some important actions on the page. Thus you encapsulate all the logic to interact with the page in a single class and not spread it around hundreds of test methods. For example, the homepage of Dropthings has a link for showing a widget gallery. When clicked, it performs an async update and loads a gallery of widgets on the page.
Here’s how a Page Adapter encapsulates these:
[Page(UrlRegex=@"Default\.aspx")]
public class HomePage : Page
{
[FindBy(Id = "TabControlPanel_ShowAddContentPanel")]
public Link AddStuffLink;
[FindBy(ClassRegex = "newtab_add*")]
public Link AddNewTabLink;
public void ShowAddStuff()
{
AddStuffLink.Click();
}
public void AddNewTab()
{
AddNewTabLink.Click();
}
public Table WidgetDataList
{
get
{
return base.Document.Table
("TabControlPanel_WidgetListControlAdd_WidgetDataList");
}
}
public List<Link> AddWidgetLinks
{
get
{
return base.Document.Links.Where
(link => link.ClassName == "widgetitem").ToList();
}
}
First you define a class that inherits from Page
class found in WatiN library, not the ASP.NET Page
class. Then you add an attribute [Page(UrlRegex=””)]
where you put the page name in Regular Expression format that matches the page. This match is used to identify whether the current URL WatiN is using on the browser matches with the desired page or not. Then you define the public
properties that represent different elements on the page. It can be buttons, links, tables, textboxes, etc., any HTML element that WatiN supports. Here I have assigned some properties to represent the “Add Stuff” link that you see on Dropthings. The WidgetDataList
represents the HTML table that is generated by a DataGrid
on the server side. The AddWidgetLinks
represents the collection of links inside the WidgetDataList
.
Once you have the page adapter, you can write tests that use this page adapter to click on important elements, then wait for the response, match the response and confirm the response is correct or not.
Writing Tests using WatiN
I have used xUnit and
SubSpec to write tests following the Behavior Driven Development BDD) approach. You can read about why
I prefer the BDD approach over Test Driven Development (TDD) approach from
this article.
[Specification]
public void User_can_show_the_widget_gallery()
{
var browser = default(Browser);
var page = default(HomePage);
"Given a user on the homepage".Context(() =>
{
browser = BrowserHelper.OpenNewBrowser(Urls.Homepage);
page = browser.Page<HomePage>();
});
"When user clicks on the 'Add Stuff' link".Do(() =>
{
page.ShowAddStuff();
browser.WaitForAsyncPostbackComplete(10000);
});
"It should show the widget gallery".Assert(() =>
{
using (browser)
{
Assert.True(page.WidgetDataList.Exists);
Assert.NotEqual(0, page.AddWidgetLinks.Count);
}
});
}
The above test uses the HomePage
adapter to show the widget gallery and confirm that the gallery is displayed and there’s widget links showing on the page.
If you did not use the Page Adapter approach, then the test code will be stained with DIV IDs, link names, classes, etc. and full of Find.ById
, Find.ByClass
etc. calls to WatiN library. The Page Adapter completely hides the underlying layout of the page being tested and it makes the test code a lot more readable.
Creating Control Adapter
You can create an adapter for controls on the page, not just the whole page itself. If you have complex controls on the page, like a Login box, a data grid with buttons, a form, a calendar, etc., you can create Control Adapters out of them to make interaction with those controls simpler. The logic to deal with those controls are then encapsulated in a single class and thus not duplicated across multiple test methods.
For example, on Dropthings, each widget is a complex control. It has a header, body, close button, edit button, a title bar, etc. Interacting with these controls is complex because the ID of these elements vary from user to user as these are dynamically created for each user.
So, I have created a WidgetControl
adapter out of the widget DIV
s and everything inside those div
s. It encapsulates common operations like changing the title of the widget by clicking on the title bar, changing widget settings by clicking on “edit” link, deleting the widget by clicking on the close button, etc.
public class WidgetControl : Control<Div>
{
public override global::WatiN.Core.Constraints.Constraint ElementConstraint
{
get
{
return Find.ByClass(className => className ==
"widget" || className.Contains("widget "))
.And(Find.ById("new_widget_template").Not());
}
}
public string Title
{
get
{
return TitleLink.Text.Trim();
}
}
public Link TitleLink
{
get
{
return base.Element.Link(Find.ByClass("widget_title_label"));
}
}
public TextField TitleEditor
{
get
{
return base.Element.TextField(Find.ByClass("widget_title_input"));
}
}
public Button TitleSaveButton
{
get
{
return base.Element.Button(Find.ByClass("widget_title_submit"));
}
}
public Link CloseLink
{
get
{
return base.Element.Link(link => link.Id.EndsWith("CloseWidget"));
}
}
public Link EditLink
{
get
{
return base.Element.Link(Find.ByClass("widget_edit"));
}
}
public Div Header
{
get
{
return base.Element.Div(div => div.ClassName.Contains("widget_header"));
}
}
public void EditTitle()
{
TitleLink.Click();
}
public void SetNewTitle(string newTitle)
{
TitleEditor.TypeText(newTitle);
TitleSaveButton.Click();
}
public void Close()
{
CloseLink.Click();
}
}
Just like Page adapters, you create a control adapter by inheriting from Control<TheElement>
. You can wrap Div
, Span
, Table
, Button
, Link
, etc., any control that WatiN supports. Then you need to override the ElementConstraint
property to return a Constraint
that identifies the control on the page. A Constraint
can be a simple Find.ByClass
or Find.ById
. The constraint I have composed here is quite complex. It selects only the DIV
s having class “widget” somewhere in the class attribute of the DIV
and not having a specific ID.
Once you have control adapters, you can write tests to test the control’s behavior. For example, the following test will test the title editing behavior.
[Specification]
public void User_can_change_widget_title()
{
var browser = default(Browser);
var widgetId = default(string);
var newTitle = Guid.NewGuid().ToString();
"Given a user with a page having some widgets".Context(() =>
{
BrowserHelper.ClearCookies();
browser = BrowserHelper.OpenNewBrowser(Urls.Homepage);
});
"When user changes title of a widget".Do(() =>
{
using (browser)
{
var page = browser.Page<HomePage>();
var widget = page.Widgets.First();
widgetId = widget.Element.Id;
widget.EditTitle();
widget.SetNewTitle(newTitle);
Thread.Sleep(1000);
}
});
"It should persist the new title on next visit".Assert(() =>
{
using (browser = BrowserHelper.OpenNewBrowser(Urls.Homepage))
{
var widget = browser.Control<WidgetControl>(widgetId);
Assert.Equal(newTitle, widget.Title);
}
});
}
Here it launches the browser, finds the first widget on the page and then changes the title of the widget. Then it closes the browser, opens again and confirms the changed title really got saved in database.
Here’s how WatiN does it:
This way, you can populate form test boxes, select items in dropdowns, test if client side validation is kicking in or not and finally submit form and ensure the submission worked. With WatiN, the possibilities are limitless.
Testing Client Side Behaviors and jQuery Animations
When you delete a widget, it uses jQuery animation to remove the widget from the page as well as an async postback to notify server that a widget has been removed. This is a typical AJAX behavior. You want to keep the page responsive by giving immediate feedback on the UI while the background update process works. Such a behavior can be tested this way:
Specification]
public void User_can_delete_widget()
{
var browser = default(Browser);
var page = default(HomePage);
var deletedWidgetId = default(string);
"Given a user having some widgets on his page".Context(() =>
{
BrowserHelper.ClearCookies();
browser = BrowserHelper.OpenNewBrowser(Urls.Homepage);
browser.WaitForAsyncPostbackComplete(10000);
});
"When user deletes one of the widget".Do(() =>
{
page = browser.Page<HomePage>();
var rssWidget = RssWidgetControl.GetTheFirstRssWidget(page);
deletedWidgetId = rssWidget.Element.Id;
rssWidget.Close();
browser.WaitForAsyncPostbackComplete(10000);
});
"It should remove the widget from the page".Assert(() =>
{
using (browser)
{
Assert.False(browser.Element(deletedWidgetId).Exists);
}
});
"It should not come on revisit".Assert(() =>
{
using (browser)
{
browser.Refresh();
browser.WaitForAsyncPostbackComplete(10000);
Assert.False(browser.Element(deletedWidgetId).Exists);
}
});
}
Here the code deletes the first RSS widget. Then it confirms the widget is really gone from the page. This validates if the jQuery stuff worked or not. Then it refreshes the browser and confirms if the widget is permanently gone or not.
Here’s how it works:
Following the same approach, you can check if jquery validations are working or not. If you have jQuery grid, you can check if the grid loads data and allows editing or not. Almost any client side action can be simulated and tested this way.
Testing Drag & Drop using WatiN
This was the most difficult one. On Dropthings, you can drag & drop widgets from one place to another. In order to simulate this, I had to use JavaScript magic to raise mousedown
, mousemove
and mouseup
events programmatically.
[Specification]
public void User_can_drag_widget()
{
var browser = default(Browser);
var page = default(HomePage);
var widget = default(WidgetControl);
var position = default(int[]);
BrowserHelper.ClearCookies();
"Given a new user on a page full of widgets".Context(() =>
{
browser = BrowserHelper.OpenNewBrowser(Urls.Homepage);
browser.WaitForAsyncPostbackComplete(10000);
});
"When user drags a widget from first column".Do(() =>
{
page = browser.Page<HomePage>();
widget = page.Widgets.First();
position = browser.FindPosition(widget.Element);
var mouseDownEvent = new NameValueCollection();
mouseDownEvent.Add("button", "1");
mouseDownEvent.Add("clientX", "0");
mouseDownEvent.Add("clientY", "0");
widget.Header.FireEventNoWait("onmousedown", mouseDownEvent);
Thread.Sleep(500);
var widgetZones = browser.Divs.Filter(Find.ByClass("widget_zone_container"));
var aWidgetZone = widgetZones[0];
var widthOfWidgetZone = int.Parse
(aWidgetZone.GetAttributeValue("clientWidth"));
for (var x = 0; x <= (widthOfWidgetZone * 1.5);
x += (widthOfWidgetZone / 4))
{
var eventProperties = new NameValueCollection();
eventProperties.Add("button", "1");
eventProperties.Add("clientX", x.ToString());
eventProperties.Add("clientY", "20");
widget.Header.FireEventNoWait("onmousemove", eventProperties);
Thread.Sleep(500);
}
});
"It should move out of the column and be floating".Assert(() =>
{
using (browser)
{
var newPosition = browser.FindPosition(widget.Element);
Assert.NotEqual(newPosition[0], position[0]);
Assert.NotEqual(newPosition[1], position[1]);
}
});
}
Here’s how it works:
Here you can see the test first loads the website, then it simulates drag experience on a widget by holding the mouse down on the title bar and then moving it across the page. The test verifies that the widget did really move the position and thus confirms the jQuery plugin used for drag & drop really works.
However, one thing I could not find is how to test the drop of the widget. Although I move the widget on the second column, it does not detect that the widget has been moved to the second column and rearranges the widgets. The sortable plugin of jQuery is not getting the necessary events fired. If you can solve it, please do let me know.
Conclusion
Tests written using WatiN replace the need for human driven tests and thus shed significant time off your regular regression test suite. Moreover, it empowers developers with a way to quickly run regression tests whenever they need to, without waiting for human QA resource’s availability. When you hook it with xUnit like test frameworks and integrate with your continuous build, you can run UI tests automatically to test all the UI scenarios overnight after your nightly build and generate reports without requiring any manual intervention.
History
- 6th August, 2010: Initial post