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 (9)

  1. We have had a few questions about being able to data bind web tests to either formats that we do not

  2. awais786 says:

    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.

  3. sagivh says:

    thats a very interesting and productive solution. thanks Sean.

    However, answering awais786 question – the test framework does a "not so great" job allowing to run tests against all rows in the database, after you execute the test once you have to edit run settings to execute X times, trying to guess the number of rows will be returned by your plugin.

    yes, i know there is an option to iterate a test against each row in the database, but this plug in will not support it, and the framework will not easily show the results for each iteration.

    Hopefuly the next version will solve this.

  4. Andy's Blog says:

    We have some new features in the upcoming release of Visual Studio Team System (Orcas). I’m going to

  5. amit.tyagi says:

    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.

  6. Richard. says:

    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);

                   }

               }

           }

       }

    }

  7. Richard says:

    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);

                   }

               }

           }

       }

    }

  8. Jason says:

    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.)  

  9. This is great, saved me a lot of time. Thanks!