65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Watin 自动化浏览器和测试复杂的 ASP.NET AJAX 网站

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (12投票s)

2010年8月6日

CPOL

8分钟阅读

viewsIcon

65104

使用 Watin 自动化浏览器和测试复杂的 ASP.NET AJAX 网站

TestControlAdapterOpt.gif

引言

WatiN 是一个优秀的 .NET 库,用于编写自动化的基于浏览器的测试。它使用真实浏览器访问网站、执行操作并检查浏览器输出。与 xUnit 等单元测试库结合使用,您可以使用 WatiN 对网站执行自动化的回归测试,从而为每个版本节省大量手动测试时间。此外,WatiN 还可用于压力测试页面上的 JavaScript,因为它可以通过重复执行操作并测量 JavaScript 运行所需的时间来推动浏览器。因此,您可以测试 JavaScript 的性能,网站的渲染速度,并确保整体呈现对用户来说快速流畅。

我为 WatiN 编写了一些扩展方法,以帮助进行 AJAX 相关的测试,特别是针对 ASP.NET UpdatePanel、jQuery 以及处理元素位置。我将向您展示如何使用这些库来测试复杂的 AJAX 网站,例如我构建的 - Dropthings,它是一个使用 ASP.NET UpdatePanel、jQuery 构建 Web 2.0 界面的、由小部件驱动的 ASP.NET AJAX 门户。您可以模拟和测试 UpdatePanel 更新、AJAX 调用和 UI 更新,甚至小部件的拖放!

您可以在我的 开源项目 代码库中看到自动化测试的实现。

WatiN ASP.NET AJAX 和 jQuery 扩展

当您测试使用 UpdatePanel 的 ASP.NET AJAX 网站时,您需要确保异步回发成功完成,然后再测试页面输出的正确性。例如,您使用 WatiN 点击了一个按钮,该按钮导致 UpdatePanel 更新页面。您需要等待异步回发完成,然后才能测试正确的输出。以下是一段实现此目的的代码片段

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));
}

同样,对于 jQuery 调用,如果您使用 jQuery 的默认实现进行 AJAX 调用,则需要确保 AJAX 调用已完成,然后再测试任何 UI 更新。方法是,在页面加载完成后立即调用此代码,以便它安装一个钩子来监视 AJAX 调用

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);
} 

然后使用此代码等待 AJAX 调用完成

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";
}

有时您需要模拟用户首次访问。因此,您需要清除缓存和 cookie,然后再访问该网站。以下是实现方法。

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();
}

有时您需要知道页面上元素的位置,以便在页面上的特定位置模拟鼠标点击。以下代码段返回元素在浏览器中的位置。

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 };
}

现在您已准备好编写一些复杂的 AJAX 测试。我将向您展示一些常见场景,例如显示 UpdatePanel 中异步更新的内容,然后验证必要的 UI 元素是否存在,然后动态添加新项目到页面并检查它们是否已正确添加,甚至模拟 WatiN 的拖放操作。

使用 WatiN 创建页面适配器

WatiN 有一个名为 Page Adapter 的有用概念。您可以创建一个代表页面功能的类。例如,该类可以公开代表页面重要元素的属性。它可以提供执行某些重要页面操作的函数。这样,您就可以将与页面交互的所有逻辑封装在一个类中,而不是分散在数百个测试方法中。例如,Dropthings 的主页有一个显示小部件库的链接。点击该链接后,它会执行异步更新并加载页面上的小部件库。

AddStuffOptPlus.gif

以下是页面适配器如何封装这些内容

[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();
        }
    }

首先,您定义一个继承自 WatiN 库中的 Page 类的类,而不是 ASP.NET 的 Page 类。然后,您添加一个 [Page(UrlRegex=””)] 属性,其中以正则表达式格式放置页面名称,以匹配该页面。此匹配用于确定 WatiN 在浏览器中使用的当前 URL 是否与所需页面匹配。然后,您定义代表页面上不同元素的 public 属性。可以是按钮、链接、表格、文本框等,任何 WatiN 支持的 HTML 元素。这里我为代表 Dropthings 上看到的“添加内容”链接分配了一些属性。WidgetDataList 代表服务器端由 DataGrid 生成的 HTML 表。AddWidgetLinks 代表 WidgetDataList 中的链接集合。

有了页面适配器后,您就可以编写使用该页面适配器来点击重要元素,然后等待响应,匹配响应并确认响应是否正确的测试。

使用 WatiN 编写测试

我使用 xUnit 和 SubSpec 按照行为驱动开发 (BDD) 的方法编写测试。您可以从 这篇文章 中了解我为什么更喜欢 BDD 方法而不是测试驱动开发 (TDD) 方法。
[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);
        }
    });
}

上述测试使用 HomePage 适配器显示小部件库,并确认库已显示并且页面上有小部件链接。

如果您不使用 Page Adapter 方法,那么测试代码将充斥着 DIV ID、链接名称、类名等,并充满了对 WatiN 库的 Find.ByIdFind.ByClass 等调用。Page Adapter 完全隐藏了被测试页面的底层布局,使得测试代码更加易读。

创建控件适配器

您可以为页面上的控件创建适配器,而不仅仅是整个页面。如果您在页面上拥有复杂的控件,例如登录框、带按钮的数据网格、表单、日历等,您可以为它们创建控件适配器,以简化与这些控件的交互。处理这些控件的逻辑然后封装在一个类中,因此不会在多个测试方法中重复。

例如,在 Dropthings 上,每个小部件都是一个复杂的控件。它有标题、正文、关闭按钮、编辑按钮、标题栏等。与这些控件交互很复杂,因为这些元素的 ID 因用户而异,因为它们是为每个用户动态创建的。

WidgetLayout.png

因此,我从这些小部件 DIV 及其内部的所有内容创建了一个 WidgetControl 适配器。它封装了常见的操作,例如通过点击标题栏更改小部件的标题,通过点击“编辑”链接更改小部件设置,通过点击关闭按钮删除小部件等。

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();
    }        
} 

与 Page Adapter 类似,您可以通过继承 Control<TheElement> 来创建控件适配器。您可以包装 DivSpanTableButtonLink 等任何 WatiN 支持的控件。然后,您需要重写 ElementConstraint 属性以返回一个 Constraint 来标识页面上的控件。Constraint 可以是简单的 Find.ByClassFind.ById。我在这里组合的约束相当复杂。它仅选择类属性中包含“widget”的 DIV,并且不具有特定的 ID。

一旦有了控件适配器,您就可以编写测试来测试控件的行为。例如,以下测试将测试标题编辑行为。

[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);
        }
    });
} 

这里它启动浏览器,找到页面上的第一个小部件,然后更改小部件的标题。然后关闭浏览器,重新打开并确认更改的标题确实已保存到数据库中。

WatiN 的实现方式如下

TestControlAdapterOpt.gif

通过这种方式,您可以填充表单文本框,在下拉列表中选择项目,测试客户端验证是否生效,最后提交表单并确保提交成功。有了 WatiN,可能性是无限的。

测试客户端行为和 jQuery 动画

当您删除一个挂件时,它会使用 jQuery 动画将其从页面中移除,同时通过异步回发通知服务器已删除挂件。这是一种典型的 AJAX 行为。您希望通过在后台更新过程工作时立即在 UI 上提供即时反馈来保持页面响应。可以使用这种方式测试此类行为

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);
            }
        });
}

这里的代码删除了第一个 RSS 小部件。然后确认该小部件确实已从页面中消失。这验证了 jQuery 是否工作正常。然后它刷新浏览器并确认小部件是否永久消失。

它的工作原理如下

DeleteWidgetOpt.gif

遵循相同的方法,您可以检查 jQuery 验证是否有效。如果您有 jQuery 网格,您可以检查网格是否加载了数据并允许编辑。几乎任何客户端操作都可以通过这种方式进行模拟和测试。

使用 WatiN 测试拖放

这是最困难的部分。在 Dropthings 上,您可以将小部件从一个地方拖放到另一个地方。为了模拟这一点,我不得不使用 JavaScript 魔法以编程方式引发 mousedownmousemovemouseup 事件。

[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);

            // Start the drag
            var mouseDownEvent = new NameValueCollection();
            mouseDownEvent.Add("button", "1");
            mouseDownEvent.Add("clientX", "0");
            mouseDownEvent.Add("clientY", "0");
            widget.Header.FireEventNoWait("onmousedown", mouseDownEvent);

            Thread.Sleep(500);

            // Move to the second column widget zone container
            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]);
            }
        });
} 

它的工作原理如下

DragDropTestOpt.gif

在这里,您可以看到测试首先加载网站,然后通过按住鼠标左键并将其拖动到页面上来模拟小部件上的拖动体验。测试验证小部件确实移动了位置,从而证实了用于拖放的 jQuery 插件确实有效。

但是,我没有找到的一点是如何测试小部件的放置。尽管我将小部件移动到第二个列,但它没有检测到小部件已被移动到第二个列并重新排列了小部件。jQuery 的 sortable 插件没有触发必要的事件。如果您能解决这个问题,请告诉我。

结论

使用 WatiN 编写的测试取代了人工驱动测试的需求,从而大大缩短了常规回归测试套件的时间。此外,它还使开发人员能够随时运行回归测试,而无需等待人工 QA 资源的可用性。当您将其与 xUnit 等测试框架集成并集成到您的持续构建中时,您可以在夜间构建后自动运行 UI 测试来测试所有 UI 场景,并在没有任何手动干预的情况下生成报告。

历史

  • 2010 年 8 月 6 日:初次发布

© . All rights reserved.