Internet Explorer automation/Watin: catching navigation error codes

One question have troubled me for some time when automating Internet Explorer (actually, I am doing web testing with Watin): how to test for HTTP status codes. Finally, I figured out how to do this. The lies in an event that the InternetExplorer object raises when navigation is unsuccessful.

I ended up with writing a C# helper class:

using System.Net;
using WatiN.Core;
using SHDocVw;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Globalization;
namespace Test
{
    public class NavigationObserver
    {
        private HttpStatusCode _statusCode;
        public NavigationObserver(IE ie)
        {
            InternetExplorer internetExplorer = (InternetExplorer)ie.InternetExplorer;
            internetExplorer.NavigateError += new DWebBrowserEvents2_NavigateErrorEventHandler(IeNavigateError);
        }
        public void ShouldHave(HttpStatusCode expectedStatusCode)
        {
            if (!_statusCode.Equals(expectedStatusCode))
            {
                Assert.Fail(string.Format(CultureInfo.InvariantCulture, "Wrong status code. Expected {0}, but was {1}",
                    expectedStatusCode, _statusCode));
            }
        }
        private void IeNavigateError(object pDisp, ref object URL, ref object Frame, ref object StatusCode, ref bool Cancel)
        {
            _statusCode = (HttpStatusCode)StatusCode;
        }
    }
}

Note that I use Visual Studio test runner to run my web tests. Then, I can use this in my test:

using (IE ie = new IE())
{
    NavigationObserver observer = new NavigationObserver(ie);
    ie.GoTo("http://some.where.com");
    observer.ShouldHave(HttpStatusCode.NotFound);
}

Sending POST requests with Watin

When doing web testing using Watin, it is not trivial to be able to do a POST request to the server. However, with the help this article on microsoft.com, I was able to figure out how. I ended up with writing this class:

public class Navigator
{
    private IE _ie;

    public Navigator(IE ie) { _ie = ie; }

    public void Post(Uri baseUri, params KeyValuePair<string, object>[] postData)
    {
        object flags = null;
        object targetFrame = null;
        object headers = "Content-Type: application/x-www-form-urlencoded" + Convert.ToChar(10) + Convert.ToChar(13);
        object postDataBytes = MakeByteStreamOf(postData);
        object resourceLocator = baseUri.ToString();
        IWebBrowser2 browser = (IWebBrowser2)_ie.InternetExplorer;
        browser.Navigate2(ref resourceLocator, ref flags, ref targetFrame, ref postDataBytes, ref headers);
        _ie.WaitForComplete();
    }

    private static byte[] MakeByteStreamOf(KeyValuePair<string, object>[] postData)
    {
        StringBuilder sb = new StringBuilder();
        if (postData.Length > 0)
        {
            foreach (KeyValuePair<string, object> postDataEntry in postData)
            {
                sb.Append(postDataEntry.Key).Append('=').Append(postDataEntry.Value).Append('&');
            }
            sb.Remove(sb.Length - 1, 1);
        }
        return ASCIIEncoding.ASCII.GetBytes(sb.ToString());
    }
}

For example, I can use it like so:

using (IE ie = new IE())
{
    Navigator navigator = new Navigator(ie);
    navigator.Post(new Uri("http://www.foo.com/"), new KeyValuePair<string, object>("p", 1));
    Assert.AreEqual("OK", ie.Text);
}

Breaking encapsulation with collections

Encapsulation is one of the most important features of object orientation, but often easy to break in practice. One common mistake to make in this respect happens when creating a class that holds some sort of collection or array.

For example, let’s assume that we want to make an immutable object (meaning that its state should never change troughout its lifecycle):


public class Request
{
    private readonly string[] _acceptedTypes;
    public Request(params string [] acceptedTypes) {...}
    public string[] AcceptedTypes { get {...} }
    public bool Accepts(string type) {...}
}

We have a class that take a list of strings as parameter to the constructor. The intent is that the list of strings should never change. Typical use of the class would be something like this:


Request request = new Request("application/json", "application/x-json");
if (request.Accepts(somestring))
{
     ...
}

Here’s a naîve implementation of the class:


public class Request
{
    private readonly string[] _acceptedTypes;

    public Request(params string[] acceptedTypes)
    {
        _acceptedTypes = acceptedTypes;
    }

    public string[] AcceptedTypes { get { return _acceptedTypes; } }

    public bool Accepts(string type)
    {
        foreach (string acceptableType in _acceptedTypes)
        {
            if (acceptableType.Equals(type))
            {
                return true;
            }
        }
        return false;
    }
}

Our intent of creating an immutable object is here manifested in the ‘readonly’ keyword used when defining the member variable _acceptedTypes, and the fact that there is only a set accessor for the AcceptedTypes property. But alas, there are several issues that break our intent.

First, let’s have a look at the AcceptedTypes accessor. It allows us to write code such as this:


request = new Request("application/json", "application/x-json" );

bool accepts1 = request2.Accepts("application/javascript");
request.AcceptedTypes[1] = "application/javascript";
bool accepts2 = request.Accepts("application/javascript");

After running this code, we find that accepts1 != accepts2. We have been able to manipulate the state of the object (in other words, it is mutable). The problem is that the AcceptedTypes exposes a reference to the array of types. Alternatively, it could only expose an IEnumerable that can be used to iterate over the types:


public IEnumerable AcceptedTypes
{
get
{
    foreach (string acceptableType in _acceptedTypes)
{
yield return acceptableType;
}
    }
}

Then, it will not be possible to manipulate the state of the object by calling request.AcceptedTypes[i].
Still, there is one problem with the current implementation. We still can write code such as this:


string[] types = new string[] { "application/json", "application/x-json" };
Request request = new Request(types);
bool accepts1 = request.Accepts("application/javascript");
types[1] = "application/javascript";
bool accepts2 = request.Accepts("application/javascript");

After running this code, accepts1 != accepts2. The problem is that we pass a referene to the constructor, and the object instance stores it in its local variable. We are free to change the object that our reference points to, indirectly changing the state of the object. In order to fix this, we change the constructor code for Request:


public Request(params string[] acceptedTypes)
{
_acceptedTypes = new string[acceptedTypes.Length];
Array.Copy(acceptedTypes, _acceptedTypes, acceptedTypes.Length);
}

Getting Integrated Windows Authentication to work on your laptop

When developing an ASP.NET solution for a customer, I run the application locally on my laptop (IIS 5.1, Win XP). For a realistic test scenario I had set up a separate host name for my local computer in my hosts file. I then enabled my application for Integrated Windows Authentication, and was a bit puzzled by why it did not work.

Internet Explorer did not automatically authenticate me when wisiting the web application. The problem turned out to be that there was no Service Principal Name registered in Active Directory that associated the hostname with my computer. Hence, the client could not request a Kerberos service ticket for my web application.

The solution was to run the setspn.exe tool (available in the Windows Server 2003 Support Tools package) to create the desired SPN entry in Active Directory. For example:

setspn -A HOST/my.fake.hostname.com MYCOMPUTER

(where MYCOMPUTER is the network computer name for my workstation). You have to be a domain administrator to run this command.

The effect of running this command is that ‘HOST/my.fake.hostname.com’ will be added to the multi value field ‘servicePrincipalName’ on my computer’s entry in Active Directory.