Foliotek Developer Blog

Simplifying C# Selenium 2 Tests for ASP.NET WebForms

Our company uses Selenium to automate regression testing of important functionality on our products. One of the products uses ASP.NET Web Forms. Since the ID for controls gets changed from “txtName” to something like “ct100$contentMain$txtName”, we were using the xPath methods (ClickByXPath(), sendKeysByXPath(), anyByXPath(), etc).

The code looked like this:

Previous Test Code

Tester.Driver.clickByXPath(“//input[contains(@id, 'btnSearch')]“);  
Tester.Driver.clickByXPath(“//select[contains(@id, 'drpPrograms')]/option[contains(text(), '" + programName + "')]“);  
Tester.Driver.clickByXPath(“//input[contains(@id, 'rblOption_2')]“);  
Tester.Driver.clickByXPath(“//input[contains(@id, 'cbOption_1')]“);  
Tester.Driver.sendKeysByXPath(“//input[contains(@id, 'txtName')]“, name);

Tester.Driver.clickByLinkText(“Next”);  

Writing xPath may be enjoyable for some, but for me it got a bit tedious. So, I created some wrapper methods to make this a lot easier to create and read.

New Test Code

ClickButton(“btnSearch”);  
SelectDropDownOption(“drpPrograms”, optionName);  
SelectRadio(“rblOption_2″);  
SelectCheckBox(“cbOption_1″);  
EnterTextBox(“txtName”, name);  

Wrapper Methods

/// Select a RadioButton or an option on a RadioButtonList.  
/// If optionText is specified for a RadioButtonList, this will select that option. Otherwise it works for native RadioButtons  
/// prefix = html filter e.g. "p/" or "tr/td/"  
public void SelectRadio(string radioButtonID, string optionText = "", string prefix = "")  
{
    string path = "//" + prefix + "input[contains(@id, '" + radioButtonID + "')]";

 if (optionText != "")  
 path += "/ option[contains(text(), '" + optionText + "')]";

    ClickByXPath(path);
}

/// Select a CheckBox  
/// prefix = html filter e.g. "p/" or "tr/td/"  
public void SelectCheckbox(string checkboxID, string prefix = "")  
{
    string path = "//" + prefix + "input[contains(@id, '" + checkboxID + "')]";

 ClickByXPath(path);
}

/// Select an option on a DropDownList  
/// prefix = html filter e.g. "p/" or "tr/td/"  
public void SelectDropDownOption(string dropdownID, string optionText = "", string prefix = "")  
{
    string path = "//" + prefix + "select[contains(@id, '" + dropdownID + "')]";

 if (optionText != "")  
 path += "/ option[contains(text(), '" + optionText + "')]";

    ClickByXPath(path);
}

/// Enter text into a TextBox and optionally clear it out first.  
public void EnterTextBox(string txtName, string value, bool clearField = false)  
{
    Tester.Driver.sendKeysByXPath("//input[contains(@id,'" + txtName + "')]", value, clearField);  
 }

/// Confirm if a condition is true. If it is, output the confirmationMessage.  
public void ConfirmTrue(bool test, string confirmationMessage)  
{
    Tester.Assert(test, confirmationMessage);
}

/// Test to see if a control exists on the page.  
/// Control type = input, div, td or whatever  
public bool ControlExists(string controlType, string controlID)  
{
    string path = "//" + controlType + "[contains(@id,'" + controlID + "')]";  
 return Tester.Driver.anyByXPath(path);
}

/// Click a Button  
public void ClickButton(string buttonName, string prefix = "")  
{
    string path = "//" + prefix + "input[contains(@id, '" + buttonName + "')]";

 ClickByXPath(path);
}

public void ClickByXPath(string xPath)  
{
    Tester.Driver.ClickByXPath(xPath);
}

public void ClickByLinkText(string linkText)  
{
    Tester.Driver.clickByLinkText(linkText);
}

For more posts, see Tips and Tricks or Functional Regression Testing


Threading with Impersonation in an ASP.NET Project

Every once in a while, you might run into a need to do something that takes some time in a web app, but doesn’t require user interaction. Maybe you are processing an uploaded file (rescaling images, unzipping, etc). Maybe you are rewriting some statistical data based on new posts. Basically, something that takes minutes or hours – but isn’t that important to be interactive with the user.

You could set up a “job” in a database to be run the next time your timer runs (see http://lanitdev.wordpress.com/2010/03/16/running-a-scheduled-task/). If you don’t have a timer yet, though, that can be overkill if you don’t care that multiple jobs may run at once.

In my case, I needed to export a large volume of data to a zip file. I asked up front for an email address – and the user will receive a link to the completed zip in an email later. The job would only be performed by admins, and even then only about once a year – so there was no need to schedule the job – I could just fire it off when the user requested it.

Any easy way to do this is to use the .NET threading objects in System.Threading. Because I need to save a file, I also have one additional issue – Threads don’t automatically run under the same account that the site does, so I had to include code to impersonate a user that has write permissions.

Here’s a bit of code to get you started:

[sourcecode lang="csharp"] // param class to pass multiple values private class ExportParams { public int UserID { get; set; } public string Email { get; set; } public string ImpersonateUser { get; set; } public string ImpersonateDomain { get; set; } public string ImpersonatePassword { get; set; } } protected void btnExport_Click(object sender,EventArgs e) { // .... code to get current app user,windows user to impersonate ..... Thread t = new Thread(new ParameterizedThreadStart(DoExport)); t.Start(new ExportParams(){ UserID=CurrentUserID, Email=txtEmail.Text, ImpersonateUser = username, ImpersonateDomain = domain, ImpersonatePassword = password }); // show user 'processing' message ..... } private void DoExport(object param) { ExportParams ep = (ExportParams)param; using(var context = Security.Impersonate(ep.ImpersonateUser , ep.ImpersonateDomain, ep.ImpersonatePassword )) { // do the work here.............. } } [/sourcecode]

Here’s the relevant part of the Security class that does the impersonation:

[sourcecode lang="csharp"] using System.Runtime.InteropServices; using System.Security.Principal; // ..... public class Security { //............. public const int LOGONTYPEINTERACTIVE = 2; public const int LOGONTYPEPROVIDERDEFAULT = 0; // Using this api to get an accessToken of specific Windows User by its user name and password [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static public extern bool LogonUser(string userName, string domain, string passWord, int logonType, int logonProvider, ref IntPtr accessToken); public static WindowsImpersonationContext Impersonate() { return Impersonate("DEFAULTUSER","DEFAULTDOMAIN","DEFAULTPASSWORD"); } public static WindowsImpersonationContext Impersonate(string username, string domain, string password) { IntPtr accessToken = IntPtr.Zero; //accessToken.Debug(); var success = LogonUser(username, domain, password, LOGONTYPEINTERACTIVE, LOGONTYPEPROVIDER_DEFAULT, ref accessToken); //accessToken.Debug(); if (!success) return null; WindowsIdentity identity = new WindowsIdentity(accessToken); return identity.Impersonate(); } // .......... }[/sourcecode]


Export to Excel Class

Summary

It took me awhile, but I eventually got tired of writing export to Excel methods on each report page. So, I created a class that will handle all the heavy lifting for me. I just call the methods I want with parameters

Methods

  • Initiate Export (file name)
  • Finalize Report (StringWriter)
  • Render Control(Control, HtmlTextWriter)
  • Print Report Title (HtmlTextWriter, report title, columns to span, align center, add space after)
  • Print Report Line (HtmlTextWriter, line of data, columns to span, align center, add space after)
  • Print Report Filter Header (HtmlTextWriter, title of filter, columns to span)
  • *Print Report Filter Item *(HtmlTextWriter, item header, columns to span for header, item text, columns to span for item
  • Print Report Filter Footer (HtmlTextWriter)
  • Export Control to Excel (Control, name of file)
  • Prep Export (Control) -* replaces links and other things that can’t be exported to text*

C# Code

[sourcecode lang="csharp"]

using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections;
using System.Xml;
using System.IO;
using zip = ICSharpCode.SharpZipLib.Zip;
using System.Text.RegularExpressions;
using System.Linq;
using System.Web.UI;
using System.Web;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;

namespace Foliotek.DataAccess
{

///

/// Provides various methods for exporting data to excel.
///

public partial class ExportToExcel : DBAccess
{
private static bool IsIE
{
get
{
return System.Web.HttpContext.Current.Request.Browser.Browser.ToLower() == “ie”;
}
}

private const string ContentType = “text/csv”;

///

/// Title Sizes
///

public enum ReportTitleSize
{
XLarge = 1,
Large = 2,
Medium = 3,
Small = 4
}

public static void InitiateExport(string fileName)
{
fileName = fileName.REPLACE(cast(cast(” ” as nvarchar(max)) as nvarchar(max)),cast(cast( “_”).Replace(“&” as nvarchar(max)) as nvarchar(max)),cast(cast( ” and ” as nvarchar(max as nvarchar(max))))).REPLACE(cast(cast(“/” as nvarchar(max)) as nvarchar(max)),cast(cast( ” “).Replace(“&” as nvarchar(max)) as nvarchar(max)),cast(cast( ” and ” as nvarchar(max as nvarchar(max))))); // remove spaces — Firefox seems to not like spaces in the filename.

System.Web.HttpContext.Current.Response.Clear();
System.Web.HttpContext.Current.Response.AddHeader(“content-disposition”, “attachment;filename=” + fileName);
System.Web.HttpContext.Current.Response.Charset = “”;
Response.Cache.SetCacheability(HttpCacheability.Private);
Response.Cache.SetMaxAge(new TimeSpan(0));
System.Web.HttpContext.Current.Response.ContentType = ContentType;
}

public static void FinalizeReport(System.IO.StringWriter stringWrite)
{
System.Web.HttpContext.Current.Response.Write(stringWrite.ToString());
System.Web.HttpContext.Current.Response.End();
}

public static void RenderControl(Control c, HtmlTextWriter hw)
{
c.RenderControl(hw);
}

///

/// Print a report title
///

/// /// /// /// /// /// public static void PrintReportTitle(HtmlTextWriter hw, string reportTitle, ReportTitleSize size, int columnsToSpan, bool alignCenter, bool addSpaceAfter)
{
if (IsIE)
{
hw.WriteLine(reportTitle);
if (addSpaceAfter)
hw.WriteLine();
}
else
{
string header = “

if (alignCenter)
header += "align=\"center\"";
else
header += "align=\"left\""; header += ">” + reportTitle + “" + (int)size + ">

“;

hw.WriteLine(header);

if (addSpaceAfter)
hw.WriteLine(“
“);
}
}

///

/// Print a report title
///

/// /// /// /// /// /// public static void PrintReportLine(HtmlTextWriter hw, string data, int columnsToSpan, bool alignCenter, bool addSpaceAfter)
{
string header = “

if (alignCenter)
header += "align=\"center\"";
else
header += "align=\"left\""; header += ">” + data + “

“;

hw.WriteLine(header);

if (addSpaceAfter)
hw.WriteLine(“
“);

}

///

/// Print the header information for the filtered paramaters for a report. NOTE: Must close with PrintReportfilterFooter()
///

/// /// /// public static void PrintReportFilterHeader(HtmlTextWriter hw, string FilterTitle, int columnsToSpan)
{
if (IsIE)
hw.WriteLine(FilterTitle);
else
{
hw.WriteLine(“

“);
hw.WriteLine(“
“);
}
}

///

/// Print an item that is being filtered
///

/// /// /// /// /// public static void PrintReportFilterItem(HtmlTextWriter hw, string itemHeader, int itemHeaderColSpan, string itemText, int itemTextColSpan)
{
if (IsIE)
hw.WriteLine(itemHeader + “,” + itemText);
else
hw.WriteLine(“

“);
}

///

/// Print the ending information for the Filter
///

/// public static void PrintReportFilterFooter(HtmlTextWriter hw)
{
if (!IsIE)
hw.WriteLine(“

### ” + FilterTitle + “

” + itemHeader + “” + itemText + “
“);

hw.WriteLine(“
“);
}

public static void ExportControlToExcel(Control control, string fileName)
{
InitiateExport(fileName);
Response.ContentType = ContentType;// “application/vnd.ms-excel”;
var writer = new StringWriter();
var hw = new HtmlTextWriter(writer);
PrepExport(control);
control.RenderControl(hw);
Response.Write(writer.ToString());
Response.End();
}

private static void PrepExport(Control control)
{
Dictionary controlsToAddAfter = new Dictionary();

foreach (Control c in control.Controls)
{
if (c.Visible)
{
PrepExport(c);

Literal litText = new Literal();

// replace links with just their text
if (c is LinkButton)
{
litText.Text = ((LinkButton)c).Text;
c.Visible = false;
}
else if (c is HtmlAnchor)
{
System.IO.StringWriter sw = new System.IO.StringWriter();
HtmlTextWriter hw = new HtmlTextWriter(sw);
c.RenderControl(hw);
litText.Text = sw.ToString();
c.Visible = false;
}
// replace checkboxes with their checked value
else if (c is CheckBox)
{
litText.Text = ( (CheckBox)c ).Checked.ToString();
c.Visible = false;
}
else if (c is HtmlInputCheckBox)
{
litText.Text = ((HtmlInputCheckBox)c).Checked.ToString();
c.Visible = false;
}

// images don’t export well – replace them with their alt-text
else if (c is HtmlImage)
{
litText.Text = ( (HtmlImage)c ).Alt;
c.Visible = false;
}
else if (c is Image)
{
litText.Text = ( (Image)c ).AlternateText;
}

// various form controls that don’t export well
else if (c is DropDownList)
c.Visible = false;
else if (c is HiddenField)
c.Visible = false;
else if (c is GridView)
c.Visible = false;
else if (c is RequiredFieldValidator)
c.Visible = false;
else if (c is TextBox)
c.Visible = false;
else if (c is CustomValidator)
c.Visible = false;
else if (c is CompareValidator)
c.Visible = false;

if (litText.Text != String.Empty)
controlsToAddAfter.Add(c.Parent.Controls.IndexOf(c), litText);
}
}

foreach (var kvp in controlsToAddAfter)
{
control.Controls.AddAt(kvp.Key, kvp.Value);
}
}
}
}

[/sourcecode]


Using the Web.Config connection string with LINQ to SQL

When updating a project to use LINQ to SQL, I found an issue with deploying to multiple environments. Each environment (development, staging, live) had it's own database associated with this. Since I had the .dbml in another assembly, it was only reading from the app.config in the assembly it resided in. I was storing the database connection string in the web.config of the project so I thought it would be nice to just use that instead of the app.config.

The first thing I needed to do was to keep the .dbml file from reading from the app.config. After opening up the .dbml file, I opened the properties window for the file. In the properties window, there is a setting for "Connection". In the "Connection" dropdown I selected the "(None)" selection. That keeps the .dbml file from accessing the app.config for the database connection string.

Now I needed to get my MainDataContext to use the Web.Config connection string. For this I created a partial class for my MainDataContext and created a constructor that passed the connection string from the Web.Config.

public partial class MainDataContext  
{
    public MainDataContext()
    : base(System.Configuration.ConfigurationManager.ConnectionStrings["Database.connection.string.from.web.config"].ToString(), mappingSource)
    {
        OnCreated();
    }
}

Now when I deploy to different environments the .dbml file is accessing the correct database instead of the same one from the app.config.


Handy ASP.NET Debug Extension Method

Most of the programmers I know (myself included) don’t bother with the built in Visual Studio debugging tools. They are slow and resource intensive. Usually, its more efficient to just do one or more Response.Write calls to see key data at key steps.

That can be a hassle, though. Most objects don’t print very well. You have to create a loop or write some LINQ/String.Join to write items in a collection.

Inspiration struck – couldn’t I write an extension method on object to write out a reasonable representation of pretty much anything? I could write out html tables for lists with columns for properties, etc.

Then I thought – I love the javascript debug console in firebug. I can drill down into individual items without being overwhelmed by all of the data at once. Why not have my debug information spit out javascript to write to the debug console? That also keeps it out of the way of the rest of the interface.

Here’s the code:

[sourcecode lang="csharp"] public static void Debug(this object value) { if (HttpContext.Current != null) { HttpContext.Current.Response.Debug(value); } } public static void Debug(this HttpResponse Response, params object[] args) { new HttpResponseWrapper(Response).Debug(args); } public static void Debug(this HttpResponseBase Response, params object[] args) { ((HttpResponseWrapper)Response).Debug(args); } public static void Debug(this HttpResponseWrapper Response, params object[] args) { if (Response != null && Response.ContentType == "text/html") { Response.Write("