Custom Data Binding in Web Tests
We have had a few questions about being able to data bind web tests to either formats that we do not support or to something other than a table, such as a select statement. This post should walk you through one way of creating custom data binding. The method provided in this post is to create one class which will manage the data and then create a web test plug-in which will add the data into the web test context. To use this example, you would need to set your connection string and modify the sql statement.
Here is the class which manages the data. This class is a singleton which will be called from the plug-in. The initialize method will make the call to your data source. In this example, I am just creating a select statement against a database. The GetSqlSelectStatement is the method which will return the SQL statement to be executed. The plug-in will fetch data by calling GetNextRow. This method will return a set of Name, Value pairs. The name is the column name and the value is the column value. Each time the method is called, it will get the current row of the dataset and then advance the current position counter. If the end of the dataset is reached, it will start from the beginning again. To customize this class, you would usually only need to change the SQL statement return from GetSqlSelectStatement.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Data;
using System.Globalization;
using System.Data.OleDb;
namespace CustomBinding
{
public class CustomDS
{
//singleton instance
private static readonly CustomDS m_instance = new CustomDS();
//keep track of next row to read
private int m_nextPosition = 0;
//Is the Datasource initialized
private bool m_initialized = false;
//Datasource object
private DataSet m_datasource;
//Data table object
private DataTable m_dataTable;
//object for locking during initialization and reading
private static readonly object m_Padlock = new object();
#region Constructor
//constructor
private CustomDS()
{
}
#endregion
#region Properties
public bool Initialized
{
get
{
return m_initialized;
}
}
public static CustomDS Instance
{
get
{
return m_instance;
}
}
#endregion
#region public Methods
public void Initialize(string connectionString)
{
lock (m_Padlock)
{
if (m_datasource == null && Initialized == false)
{
//load the data
//create adapter
OleDbDataAdapter da = new OleDbDataAdapter(GetSqlSelectStatement(), connectionString);
//create the dataset
m_datasource = new DataSet();
m_datasource.Locale = CultureInfo.CurrentCulture;
//load the data
da.Fill(m_datasource);
m_dataTable = m_datasource.Tables[0];
//set the manager to initialized
m_initialized = true;
}
}
}
public Dictionary<String, String> GetNextRow()
{
if (m_dataTable != null)
{
//lock the thread
lock (m_Padlock)
{
//if you have reached the end of the cursor, loop back around to the beginning
if (m_nextPosition == m_dataTable.Rows.Count)
{
m_nextPosition = 0;
}
//create an object to hold the name value pairs
Dictionary<String, String> dictionary = new Dictionary<string, string>();
//add each column to the dictionary
foreach (DataColumn c in m_dataTable.Columns)
{
dictionary.Add(c.ColumnName, m_dataTable.Rows[m_nextPosition][c].ToString());
}
m_nextPosition++;
return dictionary;
}
}
return null;
}
private string GetSqlSelectStatement()
{
return "Select * from Customers";
}
#endregion
}
}
The next class is the Web Test Plug-in. Web Test plug-ins are called once for each iteration of the web test. This plug-in first checks to see if the data source has been initialized. If it has not been initialized, it will call the initialize method of the CustomDS class. The initialize method only takes the connection string. This plug-in checks to see if a context parameter was set on the web test that indicates what the connection string should be. If no connection string was set, it will use the default string defined in this class. After the data is initialized, it calls GetNextRow to get the data for the current iteration. Once it gets the data, it adds each column to the context. Now the data is available to use in the web test.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.VisualStudio.TestTools.WebTesting;
namespace CustomBinding
{
public class CustomBindingPlugin : WebTestPlugin
{
string m_ConnectionString = @"Provider=SQLOLEDB.1;Data Source=dbserver;Integrated Security=SSPI;Initial Catalog=Northwind";
public override void PostWebTest(object sender, PostWebTestEventArgs e)
{
}
public override void PreWebTest(object sender, PreWebTestEventArgs e)
{
//check to make sure that the data has been loaded
if (CustomDS.Instance.Initialized == false)
{
//look to see if the connection string is set as a context parameter in the web test
//if it is not set use the default string set in this plugin
if (e.WebTest.Context.ContainsKey("ConnectionString"))
{
m_ConnectionString = (string)e.WebTest.Context["ConnectionString"];
}
CustomDS.Instance.Initialize(m_ConnectionString);
}
//add each column to the context
Dictionary<string, string> dictionary = CustomDS.Instance.GetNextRow();
foreach (string key in dictionary.Keys)
{
//if the key exists, then update it. Otherwise add the key
if (e.WebTest.Context.ContainsKey(key))
{
e.WebTest.Context[key] = dictionary[key];
}
else
{
e.WebTest.Context.Add(key, dictionary[key]);
}
}
}
}
}
After adding these classes to you project, you can then set the web test plug-in on the web test. You can do this by clicking the Set Web Test Plug-in button on the web test toolbar. If you want or need to have multiple plug-ins, you will need to use a coded web test. You can have multiple plug-ins in coded test but not the declarative test.
The way to access a value from the plug-in is to surround the variable with {{…}}. If you wanted to bind one of the query string parameters to a column called CustomerName, you would set the value of the query string parameter to {{CustomerName}}
Here are the same samples in VB:
Imports System
Imports System.Collections.Generic
Imports System.Collections.Specialized
Imports System.Data
Imports System.Globalization
Imports System.Data.OleDb
Namespace CustomBinding
Public Class CustomDS
'singleton instance
Private Shared ReadOnly m_instance As CustomDS = New CustomDS()
'keep track of next row to read
Private m_nextPosition As Integer = 0
'Is the Datasource initialized
Private m_initialized As Boolean = False
'Datasource object
Private m_datasource As DataSet
'Data table object
Private m_dataTable As DataTable
'object for locking during initialization and reading
Private Shared ReadOnly m_Padlock As Object = New Object()
'constructor
Private Sub New()
End Sub
Public ReadOnly Property Initialized() As Boolean
Get
Return m_initialized
End Get
End Property
Public Shared ReadOnly Property Instance() As CustomDS
Get
Return m_instance
End Get
End Property
Public Sub Initialize(ByVal connectionString As String)
SyncLock (m_Padlock)
If (m_datasource Is Nothing) And (Initialized = False) Then
'load the data
'create adapter
Dim da As OleDbDataAdapter = New OleDbDataAdapter(GetSqlSelectStatement(), connectionString)
'create the dataset
m_datasource = New DataSet()
m_datasource.Locale = CultureInfo.CurrentCulture
'load the data
da.Fill(m_datasource)
m_dataTable = m_datasource.Tables(0)
'set the manager to initialized
m_initialized = True
End If
End SyncLock
End Sub
Public Function GetNextRow() As Dictionary(Of String, String)
If Not m_dataTable Is Nothing Then
'lock the thread
SyncLock (m_Padlock)
'if you have reached the end of the cursor, loop back around to the beginning
If m_nextPosition = m_dataTable.Rows.Count Then
m_nextPosition = 0
End If
'create an object to hold the name value pairs
Dim dictionary As Dictionary(Of String, String) = New Dictionary(Of String, String)()
'add each column to the dictionary
For Each c As DataColumn In m_dataTable.Columns
dictionary.Add(c.ColumnName, m_dataTable.Rows(m_nextPosition)(c).ToString())
Next
m_nextPosition += 1
Return dictionary
End SyncLock
End If
Return Nothing
End Function
Private Function GetSqlSelectStatement() As String
Return "Select * from Customers"
End Function
End Class
End Namespace
Imports System
Imports System.Collections.Generic
Imports System.Text
Imports Microsoft.VisualStudio.TestTools.WebTesting
Namespace CustomBinding
Public Class CustomBindingPlugin
Inherits WebTestPlugin
Dim m_ConnectionString As String = "Provider=SQLOLEDB.1;Data Source=dbserver;Integrated Security=SSPI;Initial Catalog=Northwind"
Public Overrides Sub PreWebTest(ByVal sender As Object, ByVal e As PreWebTestEventArgs)
'check to make sure that the data has been loaded
If CustomDS.Instance.Initialized = False Then
'look to see if the connection string is set as a context parameter in the web test
'if it is not set use the default string set in this plugin
If e.WebTest.Context.ContainsKey("ConnectionString") Then
m_ConnectionString = e.WebTest.Context("ConnectionString").ToString()
End If
CustomDS.Instance.Initialize(m_ConnectionString)
End If
'add each column to the context
Dim dictionary As Dictionary(Of String, String) = CustomDS.Instance.GetNextRow()
For Each key As String In dictionary.Keys
'if the key exists, then update it. Otherwise add the key
If e.WebTest.Context.ContainsKey(key) Then
e.WebTest.Context(key) = dictionary(key)
Else
e.WebTest.Context.Add(key, dictionary(key))
End If
Next
End Sub
Public Overrides Sub PostWebTest(ByVal sender As Object, ByVal e As PostWebTestEventArgs)
'do nothing
End Sub
End Class
End Namespace
Comments
Anonymous
December 15, 2006
We have had a few questions about being able to data bind web tests to either formats that we do notAnonymous
January 23, 2007
Hi i m using the above code and it is working fine.but it is not working for all rows from tables.it picks up only first row. can u please tell me how it will work for all rows from table.Anonymous
April 16, 2007
The comment has been removedAnonymous
June 27, 2007
We have some new features in the upcoming release of Visual Studio Team System (Orcas). I'm going toAnonymous
January 21, 2009
Hi i m using the above code and it is working fine.but it is not working for all rows from tables.it picks up only first row. can u please tell me how it will work for all rows from table.Anonymous
January 26, 2011
Hello. I implement a code that looks like yours but with xml file. It works fine when I execute this in one agent but not when I use multiple agents. When multiple agents were used, the system will execute the same list of test in each agent. What I want is to execute some lines with the first agent and some other with the other agents. I try to use e.WebTest.Context.WebTestIteration as key but this key is depending on the agent. I try to manage the agentCount and agent number but my tests doesn't have the same duration. I imagine I can create a web service or a database proc stoc to be sure that each agent will collect the right line but I was considering if it is possible to use a unique Id that will be consistent across all platforms? Any ideas? If people are interested, here are the code I wrote based on yours. using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.WebTesting; using System.Xml.Linq; using System.Threading; namespace LoadTest.Plugins { /// <summary> /// This class is a singleton that will collect data from XML. /// This XML should contain the list of orders for all web test executed during load test. /// </summary> public class CustomDS { #region Fields /// <summary> /// singleton instance /// </summary> private static readonly CustomDS instance = new CustomDS(); /// <summary> /// List of elements read in the xml file definition /// </summary> private List<XElement> orders = null; /// <summary> /// Indicate if the xml definition document is read /// </summary> private bool initialized = false; /// <summary> /// object for locking during initialization and reading /// </summary> private static readonly object padlock = new object(); public static readonly DateTime TestStartingTime = DateTime.Now; #endregion #region Properties /// <summary> /// Indicate if the xml definition document is read /// </summary> public bool Initialized { get { return initialized; } } public bool IsSequencial { get; set; } /// <summary> /// singleton instance /// </summary> public static CustomDS Instance { get { return instance; } } #endregion #region public Methods /// <summary> /// Load the xml document as datasource /// </summary> public void Initialize(string xmlfileName) { lock (padlock) { XDocument doc = XDocument.Load(xmlfileName); orders = doc.Descendants("OrderEntry").ToList(); IsSequencial = (orders.Count > 0) && (orders[0].Attribute("StartingTime") != null); } } /// <summary> /// Read a /// </summary> /// <param name="orderNumber">Id of the current web test to be able to collect the right parameters definition line</param> /// <returns>List of Key, value to be used as context parameters. Each keys are a copy of attibutes name and each value are corresponding values.</returns> public Dictionary<string, string> GetDefinition(int orderNumber) { lock (padlock) { if (orders == null) return null; //No orders => no parameteres if (orderNumber >= orders.Count) { //No more orders. return null; } else { Dictionary<string, string> dico = new Dictionary<string, string>(); //Copy each attributes (name, value) as key,value in the dictionary. foreach (XAttribute item in orders[orderNumber].Attributes()) { dico.Add(item.Name.LocalName, item.Value); } return dico; } } } #endregion } /// <summary> /// This plugin is used to /// </summary> public class WebTestPluginCustomBinding : WebTestPlugin { /// <summary> /// Name of the context parameter which countain the file path of the xml datasource. /// </summary> public string XMLFileNameParameterName { get { return xmlFileNameParameterName; } set { xmlFileNameParameterName = value; } } private string xmlFileNameParameterName = "XMLFileDefinition"; /// <summary> /// This method is performed before the begining of the test. We will collect parameters from xml data source for the current test. /// These parameters will be used by the robot as parameters for the test process. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public override void PreWebTest(object sender, PreWebTestEventArgs e) { //check to make sure that the data has been loaded if (CustomDS.Instance.Initialized == false) { //look to see if the connection string is set as a context parameter in the web test //if it is not set use the default string set in this plugin if (e.WebTest.Context.ContainsKey(xmlFileNameParameterName)) { string fileName = (string)e.WebTest.Context[xmlFileNameParameterName]; if (!string.IsNullOrEmpty(fileName)) CustomDS.Instance.Initialize(fileName); } } int nextOrderId = -1; if (CustomDS.Instance.IsSequencial) { nextOrderId = e.WebTest.Context.WebTestIteration - 1; } else { nextOrderId = e.WebTest.Context.WebTestUserId; } //add each column to the context Dictionary<string, string> dictionary = CustomDS.Instance.GetDefinition(nextOrderId); if (dictionary != null) { foreach (string key in dictionary.Keys) { //if the key exists, then update it. Otherwise add the key if (e.WebTest.Context.ContainsKey(key)) { e.WebTest.Context[key] = dictionary[key]; } else { e.WebTest.Context.Add(key, dictionary[key]); } } if (e.WebTest.Context.ContainsKey("StartingTime")) { e.WebTest.Context["StartingTime"] = Convert.ToInt64(e.WebTest.Context["StartingTime"]) - (DateTime.Now.Subtract(CustomDS.TestStartingTime).TotalSeconds*1000); } } } } }Anonymous
January 26, 2011
Hello. I implement a code that looks like yours but with xml file. It works fine when I execute this in one agent but not when I use multiple agents. When multiple agents were used, the system will execute the same list of test in each agent. What I want is to execute some lines with the first agent and some other with the other agents. I try to use e.WebTest.Context.WebTestIteration as key but this key is depending on the agent. I try to manage the agentCount and agent number but my tests doesn't have the same duration. I imagine I can create a web service or a database proc stoc to be sure that each agent will collect the right line but I was considering if it is possible to use a unique Id that will be consistent across all platforms? Any ideas? If people are interested, here are the code I wrote based on yours. using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.WebTesting; using System.Xml.Linq; using System.Threading; namespace LoadTest.Plugins { /// <summary> /// This class is a singleton that will collect data from XML. /// This XML should contain the list of orders for all web test executed during load test. /// </summary> public class CustomDS { #region Fields /// <summary> /// singleton instance /// </summary> private static readonly CustomDS instance = new CustomDS(); /// <summary> /// List of elements read in the xml file definition /// </summary> private List<XElement> orders = null; /// <summary> /// Indicate if the xml definition document is read /// </summary> private bool initialized = false; /// <summary> /// object for locking during initialization and reading /// </summary> private static readonly object padlock = new object(); public static readonly DateTime TestStartingTime = DateTime.Now; #endregion #region Properties /// <summary> /// Indicate if the xml definition document is read /// </summary> public bool Initialized { get { return initialized; } } public bool IsSequencial { get; set; } /// <summary> /// singleton instance /// </summary> public static CustomDS Instance { get { return instance; } } #endregion #region public Methods /// <summary> /// Load the xml document as datasource /// </summary> public void Initialize(string xmlfileName) { lock (padlock) { XDocument doc = XDocument.Load(xmlfileName); orders = doc.Descendants("OrderEntry").ToList(); IsSequencial = (orders.Count > 0) && (orders[0].Attribute("StartingTime") != null); } } /// <summary> /// Read a /// </summary> /// <param name="orderNumber">Id of the current web test to be able to collect the right parameters definition line</param> /// <returns>List of Key, value to be used as context parameters. Each keys are a copy of attibutes name and each value are corresponding values.</returns> public Dictionary<string, string> GetDefinition(int orderNumber) { lock (padlock) { if (orders == null) return null; //No orders => no parameteres if (orderNumber >= orders.Count) { //No more orders. return null; } else { Dictionary<string, string> dico = new Dictionary<string, string>(); //Copy each attributes (name, value) as key,value in the dictionary. foreach (XAttribute item in orders[orderNumber].Attributes()) { dico.Add(item.Name.LocalName, item.Value); } return dico; } } } #endregion } /// <summary> /// This plugin is used to /// </summary> public class WebTestPluginCustomBinding : WebTestPlugin { /// <summary> /// Name of the context parameter which countain the file path of the xml datasource. /// </summary> public string XMLFileNameParameterName { get { return xmlFileNameParameterName; } set { xmlFileNameParameterName = value; } } private string xmlFileNameParameterName = "XMLFileDefinition"; /// <summary> /// This method is performed before the begining of the test. We will collect parameters from xml data source for the current test. /// These parameters will be used by the robot as parameters for the test process. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public override void PreWebTest(object sender, PreWebTestEventArgs e) { //check to make sure that the data has been loaded if (CustomDS.Instance.Initialized == false) { //look to see if the connection string is set as a context parameter in the web test //if it is not set use the default string set in this plugin if (e.WebTest.Context.ContainsKey(xmlFileNameParameterName)) { string fileName = (string)e.WebTest.Context[xmlFileNameParameterName]; if (!string.IsNullOrEmpty(fileName)) CustomDS.Instance.Initialize(fileName); } } int nextOrderId = -1; if (CustomDS.Instance.IsSequencial) { nextOrderId = e.WebTest.Context.WebTestIteration - 1; } else { nextOrderId = e.WebTest.Context.WebTestUserId; } //add each column to the context Dictionary<string, string> dictionary = CustomDS.Instance.GetDefinition(nextOrderId); if (dictionary != null) { foreach (string key in dictionary.Keys) { //if the key exists, then update it. Otherwise add the key if (e.WebTest.Context.ContainsKey(key)) { e.WebTest.Context[key] = dictionary[key]; } else { e.WebTest.Context.Add(key, dictionary[key]); } } if (e.WebTest.Context.ContainsKey("StartingTime")) { e.WebTest.Context["StartingTime"] = Convert.ToInt64(e.WebTest.Context["StartingTime"]) - (DateTime.Now.Subtract(CustomDS.TestStartingTime).TotalSeconds*1000); } } } } }Anonymous
January 08, 2012
Any better method for custom DataSource in VS.NET/Team Test 2010? It seems like one should be able to inherit from Microsoft.VisualStudio.TestTools.WebTesting.DataSource, or to add to the Tables collection, but it doesn't seem to possible. Furthermore, it would be nice to somehow, add the values to the UI, such that when someone can select the columns from the values drop-down in properties. Any way to add a custom datasource to the "New Test Data Source Wizard" dialog via an add-in or extension? This seems like the solution most need (maybe wrong, but seems to be.)Anonymous
June 18, 2013
This is great, saved me a lot of time. Thanks!