Creating a smart Extraction Rule for VSTS

In doing performance testing it is extremely common to encounter situations where your test scripts need to choose a specific link on the page based on some condition.  For example, consider the following table:

User Posts
Spongebob 0
Squidward 8
Sandy 5

The test script then needs to click on either of the users that have more than one post.  Because this is usually dynamic data we have no way to predict in which positions the valid rows will appear.  The built in VSTS extraction rules don't provide a mechanism for selecting a match based on a specific condition either.  Enter the "GroupedRegexExtractionRule".  This extraction rule allows you to use the power of regular expressions and grouping to extract the data you need.

The way the ExtractionRule works is that you create groups using parens () inside of a regular expression that matches the items of interest.  You can the select which group number should be set as the value of the context parameter.  You can also select another group to use as a condition.  So in the example of the table above, I use the following regular expression:

<td><a href="(.*?)">.*?</a></td><td>(\d+)</td>

The first group (green) is the link that I want to save into the context parameter.  The second group (blue) is the conditional group that should be a value greater than 0.

This extraction rule also allows you to choose the first, last, random, or a specific index to specify which valid match to use.

Here is the code (I will try and post an update later that provides drop downs for the ConditionOperator and UseMatch properties):

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.WebTesting;
using System.Text.RegularExpressions;
using System.ComponentModel;

namespace ExtractionRulesClassLibrary
{
    public class GroupedRegexExtractionRule : ExtractionRule
    {
        public override string RuleName
        {
            get
            {
                return "Grouped Regular Expression";
            }
        }

        public override string RuleDescription
        {
            get
            {
                return "Use a regular expression's groups to dynamically extract a value.";
            }
        }

        private String _Regex = "";
        [Description("The regular expression to use")]
        [DefaultValue("")]
        public String Regex
        {
            get
            {
                return _Regex;
            }
            set
            {
                _Regex = value;
            }
        }

        private int _ParameterGroup = 0;
        [Description("The capture group number to use to set the parameter")]
        [DefaultValue(0)]
        public int ParameterGroup
        {
            get
            {
                return _ParameterGroup;
            }
            set
            {
                _ParameterGroup = value;
            }
        }

        private int _ConditionGroup = 0;
        [Description("The capture group to use as a condition (set to -1 to skip condition checks)")]
        [DefaultValue(0)]
        public int ConditionGroup
        {
            get
            {
                return _ConditionGroup;
            }
            set
            {
                _ConditionGroup = value;
            }
        }

        private String _ConditionOperator = ">";
        [Description("The operator to use to compare the condition group (==,>,<,<=,>=")]
        [DefaultValue(">")]
        public String ConditionOperator
        {
            get
            {
                return _ConditionOperator;
            }
            set
            {
                _ConditionOperator = value;
            }
        }

        private String _ConditionComparison = "0";
        [Description("The value to use to compare (String) against the condition group")]
        [DefaultValue("0")]
        public String ConditionComparison
        {
            get
            {
                return _ConditionComparison;
            }
            set
            {
                _ConditionComparison = value;
            }
        }

        private String _UseMatch = "first";
        [Description("The match to use (first, last, random, or an int)")]
        [DefaultValue("first")]
        public String UseMatch
        {
            get
            {
                return _UseMatch;
            }
            set
            {
                _UseMatch = value;
            }
        }

        private Boolean _UseSingleline = true;
        [Description("Use RegexOptions.Singleline")]
        [DefaultValue(true)]
        public Boolean UseSingleline
        {
            get
            {
                return _UseSingleline;
            }
            set
            {
                _UseSingleline = value;
            }
        }

        private Boolean _UseIngoreCase = true;
        [Description("Use RegexOptions.IgnoreCase")]
        [DefaultValue(true)]
        public Boolean UseIngoreCase
        {
            get
            {
                return _UseIngoreCase;
            }
            set
            {
                _UseIngoreCase = value;
            }
        }

        private Boolean meets_condition(String capture)
        {
            double a;
            double b;

            switch (_ConditionOperator)
            {
                case "==":
                    return capture == _ConditionComparison;

                case ">":
                    if ((double.TryParse(capture, out a)) && (double.TryParse(_ConditionComparison, out b)))
                    {
                        return a > b;
                    }
                    return capture.CompareTo(_ConditionComparison) > 0 ? true : false;

                case "<":
                    if ((double.TryParse(capture, out a)) && (double.TryParse(_ConditionComparison, out b)))
                    {
                        return a > b;
                    }
                    return capture.CompareTo(_ConditionComparison) < 0 ? true : false;

                case ">=":
                    if ((double.TryParse(capture, out a)) && (double.TryParse(_ConditionComparison, out b)))
                    {
                        return a > b;
                    }
                    return capture.CompareTo(_ConditionComparison) >= 0 ? true : false;

                case "<=":
                    if ((double.TryParse(capture, out a)) && (double.TryParse(_ConditionComparison, out b)))
                    {
                        return a > b;
                    }
                    return capture.CompareTo(_ConditionComparison) <= 0 ? true : false;

            }
            return false;
        }

        private Match choose_match(List<Match> metconditions)
        {
            int index;
            if (metconditions.Count > 0)
            {
                if (_UseMatch == "first")
                {
                    return metconditions[0];
                }
                else if (_UseMatch == "last")
                {
                    return metconditions[metconditions.Count - 1];
                }
                else if (_UseMatch == "random")
                {
                    Random rand = new Random(DateTime.Now.Millisecond);
                    return metconditions[rand.Next(0, metconditions.Count)];
                }
                else if (int.TryParse(_UseMatch, out index))
                {
                    if (metconditions.Count > index)
                    {
                        return metconditions[index];
                    }
                }
                throw new Exception("Invalid UseMatch field in Extraction Rule for " + this.ContextParameterName);
            }
            throw new Exception("No matches found in Extraction Rule for " + this.ContextParameterName);
        }

        public override void Extract(object sender, ExtractionEventArgs e)
        {
            if (e.Response.HtmlDocument != null)
            {
                //do work to find result
                Regex extractor;
                if (_UseSingleline && _UseIngoreCase)
                {
                    extractor = new Regex(_Regex, RegexOptions.IgnoreCase | RegexOptions.Singleline);
                }
                else if (_UseIngoreCase)
                {
                    extractor = new Regex(_Regex, RegexOptions.Singleline);
                }
                else if (_UseSingleline)
                {
                    extractor = new Regex(_Regex, RegexOptions.IgnoreCase);
                }
                else
                {
                    extractor = new Regex(_Regex);
                }

                MatchCollection matches = extractor.Matches(e.Response.BodyString);
                List<Match> metconditions = new List<Match>();
                foreach (Match match in matches)
                {
                    if ((match.Groups.Count > _ConditionGroup + 1) && ((match.Groups.Count > _ParameterGroup + 1)))
                    {
                        if (_ConditionGroup >= 0)
                        {
                            if (meets_condition(match.Groups[_ConditionGroup + 1].Value))
                            {
                                metconditions.Add(match);
                            }
                        }
                        else
                        {
                            metconditions.Add(match);
                        }
                    }
                }

                e.WebTest.Context.Add(this.ContextParameterName, choose_match(metconditions).Groups[_ParameterGroup + 1].Value);
                e.Success = true;
                return;

            }
            e.Success = false;
            e.Message = this.ContextParameterName + " not found";
        }
    }
}