At my customer the good old problem of needing to run tests as different users came up again. I have tried to solve this before and came up with parts of the login dialogue handler.

The problem is this: when IE starts it uses information in the HKEY Current Users registry hive to decide who it should launch IE as. This prevents using an impersonated user token to start IE.

The other way is to use System.Diagnostics.Process start to start the process (or run as), but then you can’t control IE as it is running as another user.

Well as it turns out, the solution was staring me right in the face all along and the answer is to use BOTH of these methods, not one or the other.

First we need to launch a new IE process using System.Diagnostics.Process start with different user credentials, then we need to use impersonation to fire up watin in a another thread, attatch to IE and we are away. I have included the code which needs a Refactor and tidy up in a big way, but it works.

It won’t work on Vista with IE 7 because of the attatchto bug, but that is fixed in IE8, Also it is also leaving a process behind when the test finishes.

The impersonation code is from this Microsoft blog http://blogs.msdn.com/jimmytr/archive/2007/04/14/writing-test-code-with-impersonation.aspx

Enjoy,

[TestMethod]
    public void TestMethod()
    {
        SecureString password = new SecureString();
        password.AppendChar('p');
        password.AppendChar('a');
        password.AppendChar('s');
        password.AppendChar('s');
        password.AppendChar('w');
        password.AppendChar('o');
        password.AppendChar('r');
        password.AppendChar('d');

        ProcessStartInfo psi = new ProcessStartInfo();
        psi.UserName = "localtest";
        psi.Password = password;
        psi.UseShellExecute = false;
        psi.LoadUserProfile = true;
        psi.FileName = "c:\\Program Files\\Internet Explorer\\iexplore.exe";
        psi.Arguments = "about:blank";

        Process proc = new Process();
        proc.StartInfo = psi;
        proc.Start();

        t.Join();

        proc.Kill();
    }

    private static void DoWorkAs(object o)
    {
        User u = o as User;

        IntPtr hToken = IntPtr.Zero;
        IntPtr hTokenDuplicate = IntPtr.Zero;

        if (Win32.LogonUser(u.UserName, u.Domain, u.Password, 2 /*LOGON32_LOGON_INTERACTIVE*/, 0 /*LOGON32_PROVIDER_DEFAULT*/, out hToken))
        {
            if (Win32.DuplicateToken(hToken, 2, out hTokenDuplicate))
            {
                WindowsIdentity windowsIdentity = new WindowsIdentity(hTokenDuplicate);
                WindowsImpersonationContext impersonationContext = windowsIdentity.Impersonate();

                // domain\username
                Console.WriteLine(" Thread 2 : " + WindowsIdentity.GetCurrent().Name);

                IE ie = IE.AttachToIE(Find.ByUrl("about:blank"));

                ie.GoTo(@"http://www.google.com/");
                ie.TextField(Find.ByName("q")).TypeText("WatiN");
                ie.Button(Find.ByName("btnG")).Click();

                Assert.IsTrue(ie.ContainsText("WatiN"));
                ie.GoTo("about:blank");

                //revert
                impersonationContext.Undo();
                Console.WriteLine(WindowsIdentity.GetCurrent().Name);
            }
        }
        if (hToken != IntPtr.Zero) Win32.CloseHandle(hToken);
        if (hTokenDuplicate != IntPtr.Zero) Win32.CloseHandle(hTokenDuplicate);
    }

    public class User
    {
        public User(string u, string d, string p)
        {
            Domain = d;
            UserName = u;
            Password = p;
        }
        public string UserName;
        public string Domain;
        public string Password;
    }
    public class Win32
    {
        // P/Invoke snask
        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool LogonUser(
            string lpszUsername,
            string lpszDomain,
            string lpszPassword,
            int dwLogonType,
            int dwLogonProvider,
            out IntPtr phToken);

        [DllImport("advapi32.dll", SetLastError = true)]
        public extern static bool DuplicateToken(IntPtr ExistingTokenHandle, int
           SECURITY_IMPERSONATION_LEVEL, out IntPtr DuplicateTokenHandle);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool CloseHandle(IntPtr hHandle);

    }

The third site in the stack overflow trilogy,superuser.comlaunched yesterday. I can say quite proudly that I have the 25th highest rep of anyone on the site. For now. Here is a pic to remember the moment, as I am sure I won’t stay there for long.

SuperUserPage1.jpg

I am pretty proud of our WatiN stack, it is in it’s third major version now and is an absolute joy to work with.

After seeing some other’s code samples online I thought that I should share some of the details so people can improve what they are doing. We recently hit a new one day record with one of our SDE/T’s writing 72 new test cases in a single day.

How it works

Our stack is broken down into four key layers, the test, a logical layer, a physical layer and then WatiN itself. If we are working on a Winforms application, we replace WatiN with an in-house developed stack that uses the Windows UI automation API’s. Our test data is stored in a SQL express database and we have a generated data access layer, which keeps the cost of creating CRUD methods very low. One of our tests looks like this:

[TestMethod]
public void VerifyCustomerCreation()
{
    Logical.Customer.CreateNewCustomer("Default Customer");
    Verification.Customer.VerifyCustomerCreated();
}

And that’s it. The tests are written in the business domain of the application that we are testing, not in a pile of clicks and set values. The next level down the “logical” layer is called by the tests and looks like this:

public static void CreateNewCustomer(string customerName)
{
    Physical.Customer.GoToNewCustomerPage();
    Physical.Customer.EnterNewCustomerInfo(customerName);
    Physical.Customer.SaveNewCustomer();
}

We build our logical layer up by combining smaller atomic actions, this allows us to heavily use the DRY (don’t repeat yourself) approach.

Our physical layer is where the rubber hits the road and it looks like this:

public static void EnterNewCustomerInfo(string customerName)
{
    ControlHandler conroller = new ControlHandler();
    TestData.Customer customerDetails = TestData.Customer.GetCustomerDetails(customerName);

    controller.SetValue(NewCustomerPage.txtFirstName, customerDetails.FirstName);

    controller.Invoke(NewCustomerPage.btnSave);
}

The control handler fully abstracts WatiN, or another automation tool so we interact with any control by typically only calling either SetValue, Invoke or GetValue.

To define our controls we have an in-house developed tool where we drag a magnifying glass (Spy++ style) onto a page and it then captures all the controls and gives us a generated static class.

This stack is the result of a lot of investment in our time and represents, in my opinion, state of the art when it comes to web test automation. But then again, I am a bit biased because I wrote it :-) .

With the latest commit to the http://code.google.com/p/mcwatin/ repository can now open and close firefox all by itself. :-)

The next feature to add is detection if a browser is open with jssh running and to re-use that browser instead of always launching a new one, or close as appropriate.

On thing that I have noticed is that the lack of a debugger in mono develop on the mac is really slowing me down as I can’t easily follow the code execution.