So far we've seen that load testing using Visual Studio webtests is a pretty effective solution. For functional testing, however, the request-based approach leaves something to be desired.

Visual Studio 2010 includes built-in support for coded UI tests that include testing with browsers but until that goes live, there is the free WebAii framework from Art of Test.

WebAii allows you to create Visual Studio unit tests that enjoy programmatic control over browsers, the DOM and JavaScript to create powerful and flexible functional web tests. Tests can be executed on both IE and Firefox. My favoured approach is to use Firebug in Firefox to identify page elements and IE for test execution.

WebAii installation

The WebAii installer creates a new unit test template that contains all the references you need for functional web testing. It is important to realise that you also need to configure your browser(s) properly to run WebAii tests. If your version of IE is as hacked about as mine is, you may also need to specifically enable the WebAii plug-in.

WebAii objects

The most important WebAii objects you will encounter are in 3 main classes:

Simple example

A classic log in process: navigate to the home page, enter a user id and password, then click a button:


[TestMethod]
public void LogInTest()
{
    //Launch a browser instance
    Manager.LaunchNewBrowser(BrowserType.InternetExplorer);
    //navigate to the home page
    ActiveBrowser.NavigateTo("http://www.mytestsite.com/home.html");
    //enter log in details
    ActiveBrowser.Find.ByName<HtmlInputText>("userid").Text = "validID";
    ActiveBrowser.Find.ByName<HtmlInputPassword>("password").Text = "validPWD";
    ActiveBrowser.Find.ByAttributes<HtmlInputSubmit>("value=Login").Click();
}

The first two lines should be self-explanatory. The last 3 rely on the powerful Find class.
I've used some basic examples here to show how you use Find to address an element by specifying how to find it, the element type and the specific value for that element. Later examples will show more complex usage.

Dealing with pop-ups

WebAii can be instructed to handle pop-ups automatically. These can be new browser windows or dialogs. You can set up default actions at the Manager level:


//watch for any popup windows
Manager.SetNewBrowserTracking(true);
//handle alerts by pressing OK
Manager.DialogMonitor.AddDialog(new AlertDialog(ActiveBrowser, DialogButton.OK));
//handle IE "do you want to close?" dialog by pressing Yes
GenericDialog IECloseDialog = new GenericDialog(ActiveBrowser, "", true);
IECloseDialog.DismissButton = DialogButton.YES;
Manager.DialogMonitor.AddDialog(IECloseDialog);

Data-driven testing - single CSV file

There is a form of data-driven testing built into Visual Studio unit tests. This allows you to bind a datasource and run the test for every record in the data either in sequence or at random. The above test can be altered to automatically test a range of login attempts:


[DataSource("Microsoft.VisualStudio.TestTools.DataSource.CSV", 
    "|DataDirectory|\\Authentication.csv", "Authentication#csv", 
    DataAccessMethod.Sequential), DeploymentItem("Testing\\Authentication.csv"), TestMethod]
public void LogInTest()
{
    //Launch a browser instance
    Manager.LaunchNewBrowser(BrowserType.InternetExplorer);
    //navigate to the home page
    ActiveBrowser.NavigateTo("http://www.mytestsite.com/home.html");
    //enter log in details
    string LoginName = System.Convert.ToString(TestContext.DataRow["Logid"]);
    string LoginPassword = System.Convert.ToString(TestContext.DataRow["Password"]);
    ActiveBrowser.Find.ByName<HtmlInputText>("userid").Text = LoginName;
    ActiveBrowser.Find.ByName<HtmlInputPassword>("password").Text = LoginPassword;
    ActiveBrowser.Find.ByAttributes<HtmlInputSubmit>("value=Login").Click();
}

The corresponding csv file would look something like:
TestCase,Logid,Password
1,validID,validPWD
2,INvalidID,validPWD
3,validID,INvalidPWD
...and so on.
Note that only one binding can be applied to a unit test.

Data-driven testing - SQL tables

There is nothing to stop you from accessing data yourself, of course. For instance, using the exact same SQL abstraction methods as we developed for JAWS:


public void SQLexecuteNonQuery(string SQLstr)
{
    SqlConnection SQLconn = new SqlConnection(
        "Data Source=testSQLserver;Initial Catalog=TestDB;Integrated Security=True");
    SqlCommand SQLcmd = new SqlCommand(SQLstr, SQLconn);
    SQLconn.Open();
    SQLcmd.ExecuteNonQuery();
    SQLconn.Close();
}
public DataTable SQLexecuteQuery(string SQLstr)
{
    SqlConnection SQLconn = new SqlConnection(
        "Data Source=testSQLserver;Initial Catalog=TestDB;Integrated Security=True");
    SqlCommand SQLcmd = new SqlCommand(SQLstr, SQLconn);
    DataSet ds = new DataSet();
    SqlDataAdapter da = new SqlDataAdapter(SQLcmd);
    da.Fill(ds, "reading");
    SQLconn.Close();
    return ds.Tables[0];
}    
We can use the data from SQL tables at any point in our tests. For instance, to test menus we can use one table to hold the menu details and another to specify some text to find in a particular frame once the menu item has been clicked:

public void UAT_Menu_Item_Tests()
{
    DataTable dt = SQLexecuteQuery("SELECT * FROM MenuItems");
    foreach (DataRow dr in dt.Rows)
    {
        string thisMenuFrame = dr["MenuFrame"].ToString(); 
        string thisSelectMenu = dr["SelectMenu"].ToString();
        string thisMenuItem = dr["MenuItem"].ToString();
        ClickMenu(thisMenuFrame, thisSelectMenu, thisMenuItem);

        string thisContext = dr["Context"].ToString();
        DataTable cct = SQLexecuteQuery("SELECT * FROM ContentChecks "
            + "WHERE Context LIKE'" + thisContext + "';");
        foreach (DataRow ccr in cct.Rows)
        {
            string thisCheckFrame = ccr["CheckFrame"].ToString();
            string thisCheckText = ccr["CheckText"].ToString();
            Verify_TextInFrame(thisCheckFrame, thisCheckText);
        }
    }
}
Where the tables look like:
MenuItems
MenuFrameSelectMenuMenuItem
middle_frameViewPersonal Details
middle_frameViewPortfolio
middle_frameChangePersonal Details
middle_frameChangePortfolio
ContentChecks
CheckFrameCheckText
middle_frameYour Details
middle_frameYour Portfolio
middle_frameChange Your Details
middle_frameChange Your Portfolio


Content and Action Handlers

Note that the example above relies on calls to ClickMenu() and Verify_TextInFrame(). These are among a set of routines that are going to be used so often that I've separated them out into their own methods:
These common routines introduce lots of new ideas:

Verify_TextInFrame()

As I may have mentioned previously, the AUT relies on frames, so to check for expected text on a page you also need to specify the frame.


public void Verify_TextInFrame(params string[] CheckDetails)
{
    string thisCheckText = "";
    Browser thisFrame = ActiveFrame();
    if (0 == CheckDetails.GetLength(0))
    {
        thisCheckText = System.Convert.ToString(TestContext.DataRow["CheckText"]);
    }
    else
    {
        thisFrame = ActiveFrame(CheckDetails[0]);
        thisCheckText = CheckDetails[1];
    }
    Log.WriteLine("Verifying text: " + thisCheckText);
    Assert.IsTrue(thisFrame.ContainsText(thisCheckText));
}

ActiveFrame()

Since the AUT is based on frames, at any point in our tests we need to know whether we are addressing an entire page (represented by the ActiveBrowser object) or a specific frame.


public Browser ActiveFrame(params string[] FrameName)
{ //if the CheckFrame is a frame, return that, otherwise return the ActiveBrowser
    Browser AUTframe = ActiveBrowser;
    AUTframe.WaitUntilReady();
    AUTframe.RefreshDomTree();
    string AUTfName = "";
    if (0 == FrameName.GetLength(0))
    {
        AUTfName = System.Convert.ToString(TestContext.DataRow["CheckFrame"]);
    }
    else
    {
        AUTfName = FrameName[0];
    }
    if (AUTfName != "")
    {
        AUTframe = ActiveBrowser.Frames[ePAfName];
        AUTframe.WaitUntilReady();
        AUTframe.RefreshDomTree();
        Assert.IsNotNull(AUTframe);
    }
    return AUTframe;
}

Perform_Action()

This is a good example of the different Find methods available. The parameters are based on finding a particular type of control in a frame (or page).


public void Perform_Action(string thisControlFrame, string thisControlType, string thisControlID, string thisControlValue)
{
    switch (thisControlType)
    {
        case "button":
            {
                HtmlInputButton thisButton =
                    ActiveFrame(thisControlFrame).Find.ByAttributes<HtmlInputButton>("value=" + thisControlID);
                thisButton.InvokeEvent(ScriptEventType.OnClick);
                break;
            }
        case "radio":
            {
                HtmlInputRadioButton thisRadio =
                    ActiveFrame(thisControlFrame).Find.ByAttributes<HtmlInputRadioButton>("value=" + thisControlID);
                thisRadio.Check(true, true);
                break;
            }
        case "select":
            {
                HtmlSelect thisSelect =
                    ActiveFrame(thisControlFrame).Find.ByName<HtmlSelect>(thisControlID);
                thisSelect.SelectByValue(thisControlValue);
                break;
            }
        case "text":
            {
                HtmlInputText thisText =
                    ActiveFrame(thisControlFrame).Find.ById<HtmlInputText>(thisControlID);
                thisText.Text = thisControlValue;
                break;
            }
        case "link":
            {
                HtmlAnchor thisAnchor =
                    ActiveFrame(thisControlFrame).Find.ByAttributes<HtmlAnchor>("href=~" + thisControlID);
                thisAnchor.Click();
                break;
            }
        case "menu":
            {
                ClickMenu(thisControlFrame, thisControlID, thisControlValue);
                break;
            }
    }
}
    
The control is uniquely identified by id, name or value and either some default action is executed or the control is set to a particular value.
Note that this method is not based on the MenuItems and ContentChecks tables above, but on the Workflow Driven Testing tables.

ClickMenu()

In the AUT, menus are rendered as divs inside table cells and the menu items appear when hovering over the top-level menu. It's certainly not pretty or efficient and it made this whole process very "interesting".


public void ClickMenu(string thisMenuFrame, string thisSelectMenu, string thisMenuItem)
{
    Log.WriteLine("Clicking menu: " + thisSelectMenu + " - " + thisMenuItem;
    ActiveFrame(thisMenuFrame).Find
        .ByContent<HtmlTableCell>(thisSelectMenu, FindContentType.TextContent)
        .InvokeEvent(ScriptEventType.OnMouseOver);
    FindParam fp = new FindParam(FindType.Content, thisMenuItem, "class=dropdownMenu");
    fp.ContentType = FindContentType.InnerText;
    ActiveFrame(thisMenuFrame).Find
        .ByParam<HtmlDiv>(fp)
        .InvokeEvent(ScriptEventType.OnClick);
}

Workflow Testing

So, after all that, we have the ability to navigate a website, check the content and perform specific actions. We can also drive the tests from data held in SQL, so the logical next step is Workflow Driven Testing.