Introduction
This article introduces some advanced features of Enum usage in C# to show how powerful it could be to define and extract a rich set of information to/from the Enum types.
More detail of its application can be found in TERESA: a compact WebDriver Enabler.
Background
The tips presented here have been used by me in some previous projects before to achieve succinct codes with strongly typed feature of Enum, strangely enough, I have not found similar discussion on them yet. As a key concept of my ongoing Automation Testing framework over WebDriver, I have defined various Enum types to store the CSS locator information conveniently.
Generally, the name of these Enum types are composed of two parts: HTML tagnames and CSS locator mechnism
In this article, I would use some extracted codes to demonstrate how to convey information by their type name, entries with attributes and method extension. Finally, I would summarize the advantages of Enum over Strings.
About Enumeration
Refer to this. An enumeration is a set of named constants whose underlying type is any integral type. Although an enum is declared using syntax that does not look at all object oriented, the fact is that in the .NET Framework an enum type is a regular type with its own well-defined inheritance hierarchy. Enum types are not allowed to have an object model exposing their own set of methods, properties, and events. An object model exists, however, on the base class of all enums—System.Enum.
Though not as flexible as its peer in JAVA, strongly classed .NET Enumeration is still valuable as a collection of constant values sharing some common points and the same base class of System.Enum.
About CSS Selector
With the Selenium IDE, there are various ways of targeting an element or groups of elements within the HTML document and CSS Selectors are strongly recommended over Xpaths.
A handy summary of common CSS selectors can be found here.
According to this article, the order of more to less efficient CSS selectors goes thus:
- ID, e.g.
#header
- Class, e.g.
.promo
- Type, e.g.
div
- Adjacent sibling, e.g.
h2 + p
- Child, e.g.
li > ul
- Descendant, e.g.
ul a
- Universal, i.e.
*
- Attribute, e.g.
[type="text"]
- Pseudo-classes/-elements, e.g.
a:hover
Although there is no handy way to locate one element by its text as XPath does, it can be supported with a LINQ query after getting the collection of possible candidate elements. In addition, locating element by text is apparently the most inefficient way that could be avoided by using other mechnism such as using sibling/parent relationships, or mechanisms like nth-child(n) and nth-of-type(n). Considering its efficiency and readability, the framework of mine is based on the CSS selector.
About WebDriver
Selenium WebDriver is a tool for automating testing web applications, and in particular to verify that they work as expected. It aims to provide a friendly API that's easy to explore and understand, which will help make your tests easier to read and maintain. An easy way to get started is this example, which searches for the term “Cheese” on Google and then outputs the result page’s title to the console.
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.UI;
class GoogleSuggest
{
static void Main(string[] args)
{
IWebDriver driver = new FirefoxDriver();
driver.Navigate().GoToUrl("http://www.google.com/");
IWebElement query = driver.FindElement(By.Name("q"));
query.SendKeys("Cheese");
query.Submit();
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
wait.Until((d) => { return d.Title.ToLower().StartsWith("cheese"); });
System.Console.WriteLine("Page title is: " + driver.Title);
driver.Quit();
}
}
This article focused only on two lines of codes in this example, that are:
IWebElement query = driver.FindElement(By.Name("q"));
query.SendKeys("Cheese");
The meaning of the codes of FindElement() and SendKeys() are straightforward, the WebDriver would locate the query element whose name is "q" and input "Cheese" to it.
However, there are some inconvenience in this common practice proposed by Selenium:
- The locator of the element (in this example, search some text field whose name is "q") is coupled with the codes.
- If the webpage changed and the query field need to be located by "ID" or the "Name" of "q" changed to "p", then there would be multiple places to be updated.
- There is thus no way to divide the work between a C# developer and a webpage designer.
- Although constant string, for example "query", can be used to store "q", the mechanism behind is hard to included and it would be quite confusing when there could be multiple elements with the same name.
- The type of query element is ambiguious.
- First, it means the driver has to query all elements within the page before capring their names with "q", apparently, the locating of the element is not so efficient as specifying it as a "textarea" or "input" first.
- Second, it is not very easy to extend the IWebElement to introducing more features to some specific elements. For example, like in my previous article Operate WebDriver Elements Efficiently, the table element should have quite different behaviour than a text or button field. But of course, it can happen only when the element has been identified as a specific type first.
- Finally, it is not readily understandable without reading the source of the web pages to decide if the codes is correctly or not. For example, if the element whose name is "q" is actually a button, then the codes can run, but there is no way to see the automation test is not running as expected in advance.
- There are still too much redundancy and typo-prone. It is not apparent in this simple example, but when it comes to a large project where there are handreds or thousands of elements, keep typing "FindElement(By.Something("somekey"))" and "SendKeys()" is tedious and not necessary.
My Expections
To address these concerns, there are several things could be done to improve the productivity of automation testing design.
- Split the element locators from the executable codes. That is, the mechanism and keyword of the element locators are defined seperately from the codes under [test] part. So if the webpage changed, only the locators need to be updated accordingly.
- Identify the type of the element as early as possible. As a result, the WebDriver can screen out elements whose tagnames are irrelevant, and such identification means we can operate elements of different types differently after encapsulating relavant functions to different types of elements.
- Identify the mechanism used to locate the element within the Enum type names.
- Group locators with same mechanism and same element type together by assigning different Enum entries with different Enum.Value(). This is particularly useful in a typical web page where for example, many links/buttons/texts differ from others slightly.
- The test cases can be simplified with the indexer whose prototype is "string this[Enum enumValue]" which would be disclosed in articles in future. Though not implemented in my framework yet, with this uniform getter and setter methods, it is possible to define the test operations as a standalone property file that could be drafted by UI designers and loaded dynamically when execution the test scripts.
As a result, the locators of the concerned element can be defined in a file like "GooglePageObject.cs" as below:
public class GooglePageObject{
public TextByName{
[EnumMember("q")]
query
}
}
In the test codes, the codes is:
somepage[GooglePageObject.query] = "Cheese";
The following parts are used to explain how the locators can be defined and accessed with Enum.
Exploiting Potentials of Enum
Method Extensions on Enum
Enumeration types are all inherited from System.Enum which is actually a class, so we can extend Enum to achieve some convenience, and the Value() as an example can be found in "EnumExtension.cs" of the attachment.
public static class EnumExtension
{
public static readonly Dictionary<Enum, string> EnumValues = new Dictionary<Enum, string>();
public static string Value(this Enum theEnum)
{
if (EnumValues.ContainsKey(theEnum))
return EnumValues[theEnum];
EnumMemberAttribute memberAttribute = EnumMemberAttribute.EnumMemberAttributeOf(theEnum);
if (!EnumValues.ContainsKey(theEnum))
{
EnumValues.Add(theEnum, memberAttribute.Value);
}
return EnumValues[theEnum];
}
...
}
Then in the same namespace, we can use it as below:
public enum SomeEnumType { entry1, entry2};
string enumValue = SomeEnumType.entry1.Value();
Noticably, there is a Dictionary<Enum, string> to cache the values of any Enum entries for efficiency and consistence consideration. Because the strongly typed feature of Enum, there could be multiple entries sharing the same Enum typename and member name but in different namespace, just as the two "ButtonById.button1" in section "Identify Both Element Types and Locator Mechanisms in EnumTypeAttribute"; and they can have different values as can be validated in "EnumMemberAttributeTests.cs" within AdvancedEnumUnitTest:
[Test]
public void EnumMemberAttribute_MembersOfSameTypeAndEntryName_WithDifferentValues()
{
string value1 = ButtonById.button1.Value();
Console.WriteLine("Value of ButtonById.button1: " + value1);
string value2 = Fragment.ButtonById.button1.Value();
Console.WriteLine("Value of Fragment.ButtonById.button1: " + value2);
Assert.AreNotEqual(value1, value2);
}
Assigning Attributes to both Enum Type and Member
Attributes provide a powerful method of associating declarative information with C# code (types, methods, properties, and so forth). Once associated with a program entity, the attribute can be queried at run time and used in any number of ways.
Unlike string, Enum is fundamentally a special kind of class, so we can associate attributes to both its types and member entries.
The attribute of EnumTypeAttribute associated with the Enum types:
[AttributeUsage(AttributeTargets.Enum, Inherited = false, AllowMultiple = false), Serializable]
public class EnumTypeAttribute : Attribute
{
The attribute of EnumMemberAttribute associated with the Enum member entries:
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false), Serializable]
public class EnumMemberAttribute : Attribute
{
Identify Both Element Types and Locator Mechanism in EnumTypeAttribute
The type of element is defined as HtmlTagName of Enum type, a simplified version extracting from the framework is listed as below:
public enum HtmlTagName
{
Unknown,
Button,
Link,
Text,
Radio
}
The mechanisms to get the CSS selector is defined as another Enum type Mechanism, and a simplified version extracting from the framework is listed as below:
public enum Mechanisms
{
ById ,
ByClass ,
ByCustom }
A valid type name expected is composed of any entry of HtmlTagName except "Unknown" and any entry of the Mechnisms, thus even in this simplified version, there could be 4 (Button, Link, Text and Radio) * 3 (ById, ByClass, ByCustom) = 12 combinations to define how CSS selector can be retrieved. Some examples can be found from the TestEnums.cs within the attached file:
namespace AdvancedEnumUnitTest
{
public enum ButtonById
{
button1,
[EnumMember("actualButton2Id", false, "User defined description")]
button2
}
public enum TextAllByClass
{
class1Text,
class2Text,
Text_with_space
}
public enum RadioByCustom
{
[EnumMember("input[type=radio]#radio1_Id")]
radio1
}
public enum InvalidCombination
{
Just_for_test
}
public class Fragment
{
public enum ButtonById
{
[EnumMember("otherId")]
button1
}
public enum ButtonByClass
{
class11,
class22
}
}
}
The composing of a valid Enum typename is: TagName (+ optionally "All") + Mechanisms.
The meanings of the valid Enum types appeared here are straightforward:
- ButtonById: locate the button element with its ID.
- TextAllByClass: locate the collections of text input or textarea with its class.
- RadioByCustom: locate the radio collection with customized CSS selector string.
- ButtonByClass: locate the button element with its class.
There are some points to be noticed:
- The typename itself contains enough information to generate CSS selectors accordingly, the detail will be discussed in ...
- Similar elements sharing the same tagname and similar definitions shall be grouped under the same Enum types, which would save a lot of typing. On the other hand, to refer an entry of an Enum type, the type name is always mandatory, thus providing enough clues for the developers to anticipate what kind of the element is and how Css locator is composed.
- There are two Enum types with the same name of "ButtonById": one is in the base namespace and another is within a class of Fragment. Because Enum is strongly typed, so both of these two types can be referred: refer the first type as "ButtonById" and second one as "Fragment.ButtonById" respectively.
- The mechanism of "ByCustom" implicits that CSS locators of its members shall always be defined explicitly.
- The optional "All" appeared within "TextAllByClass" means there are multiple text fields sharing the same class.
The function to analyze the Enum typename to get its meaning is contained in "EnumTypeAttribute.cs":
public const string CollectionIndicator = "All";
public const string By = "By";
public static string MechanismOptions = null;
public static Dictionary<string, EnumTypeAttribute> CachedEnumTypeAttributes = new Dictionary<string, EnumTypeAttribute>();
public static EnumTypeAttribute TypeAttributeOf(Type enumType)
{
string typeName = enumType.Name;
if (!CachedEnumTypeAttributes.ContainsKey(typeName))
{
CachedEnumTypeAttributes.Add(typeName, ParseTypeName(typeName));
}
return CachedEnumTypeAttributes[typeName];
}
private static EnumTypeAttribute ParseTypeName(string enumTypeName)
{
int byPosition = enumTypeName.IndexOf(By, StringComparison.OrdinalIgnoreCase);
if (byPosition == -1)
throw new InvalidEnumArgumentException(
"The type name of enumId must be a pattern of ***By*** for further processing");
string prefix = enumTypeName.Substring(0, byPosition);
string suffix = enumTypeName.Substring(byPosition);
bool isCollection = false;
HtmlTagName tagName = HtmlTagName.Unknown;
if (!string.IsNullOrEmpty(prefix))
{
if (prefix.EndsWith(CollectionIndicator))
{
isCollection = true;
prefix = prefix.Substring(0, prefix.Length - CollectionIndicator.Length);
}
if (!Enum.TryParse(prefix, true, out tagName))
{
throw new Exception("ElementWrapper type of " + prefix + " is not supported yet.");
}
if (tagName.Equals(HtmlTagName.Radio))
isCollection = true;
}
Mechanisms mechanism = Mechanisms.ById;
if (!Enum.TryParse(suffix, true, out mechanism))
throw new Exception(string.Format("The valid Enum Type name must be ended with one of:\r\n" + MechanismOptions));
return new EnumTypeAttribute(isCollection, mechanism, tagName);
}
Generally, it tries to match the typename with items of HtmlTagName and Mechanisms. Again, a cache is used to avoid parsing the same typename multiple times. However, only the Enum typename as a string is used as the key based on the assumption that they are composed strictly in the expected format.
To generate CSS selectors by using HtmlTagName and Mechanisms, there are some format strings are associated with each entry of them in advance:
static EnumTypeAttribute()
{
#region Registering Default CSS construction formats needed by different Mechanism
EnumExtension.RegisterEnumValue(Mechanisms.ById, "{0}#{1}");
EnumExtension.RegisterEnumValue(Mechanisms.ByClass, "{0}.{1}");
EnumExtension.RegisterEnumValue(Mechanisms.ByCustom, "");
#endregion
#region Registering Default CSS construction formats needed by different Mechanism
EnumExtension.RegisterEnumValue(HtmlTagName.Button,
"{0} button{1}, {0} input[type=button]{1}, {0} input.btn{1}, {0} input.button{1}, {0} div[role=button]{1}");
EnumExtension.RegisterEnumValue(HtmlTagName.Unknown, "{0} {1}");
EnumExtension.RegisterEnumValue(HtmlTagName.Link, "{0} a{1}");
EnumExtension.RegisterEnumValue(HtmlTagName.Text, "{0} input[type]{1}, {0} textarea{1}");
EnumExtension.RegisterEnumValue(HtmlTagName.Radio, "{0} input[type=radio]{1}");
var htmlTagNames = Enum.GetValues(typeof(HtmlTagName)).OfType<HtmlTagName>();
foreach (HtmlTagName htmlTagName in htmlTagNames)
{
if (!EnumExtension.EnumValues.ContainsKey(htmlTagName))
EnumExtension.RegisterEnumValue(htmlTagName,
"{0} " + htmlTagName.ToString().ToLowerInvariant() + "{1}");
}
#endregion
var values = Enum.GetValues(typeof(Mechanisms)).OfType<Enum>().ToList();
StringBuilder sb = new StringBuilder();
foreach (var theEnum in values)
{
sb.AppendFormat("{0}, ", theEnum);
}
MechanismOptions = sb.ToString().Trim(new char[] { ' ', ',' });
}
So for example, "{0}#{1}" is associated with Mechanisms.ById, and "{0} button{1}, {0} input[type=button]{1}, {0} input.btn{1}, {0} input.button{1}, {0} div[role=button]{1}", which represents 5 common expressions of Button element in HTML, is associated with HtmlTagName.Button. Then using String.Format() two times, we can get the expected CSS selector as will be discussed in next section.
Identify Elements and Generate CSS Selector with EnumMemberAttribute
The EnumMemberAttribute, means to convey information inherited from the Enum Typename and element identifiers, has three constructors and multiple fields to store all kinds of information related with an element:
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false), Serializable]
public class EnumMemberAttribute : Attribute
{
public static readonly char[] EnumValueSplitters = { ' ', '=' };
...
private bool isFragmentLocator;
private string _value;
private string description;
;
private bool isCollection;
private HtmlTagName tagName;
private Mechanisms mechanism;
...
public EnumMemberAttribute(bool isFragmentLocator = false)
{
this.isFragmentLocator = isFragmentLocator;
}
public EnumMemberAttribute(string valueString = null, bool isFragmentLocator = false, string description = null)
{
this.isFragmentLocator = isFragmentLocator;
this._value = valueString;
this.description = description;
}
protected EnumMemberAttribute(string valueString, bool isFragmentLocator, string description, string css,
Mechanisms mechanism, bool isCollection = false, HtmlTagName tag = HtmlTagName.Unknown)
{
this.isFragmentLocator = isFragmentLocator;
this._value = valueString;
this.description = description;
this._css = css;
this.mechanism = mechanism;
this.isCollection = isCollection;
this.tagName = tag;
}
}
The last three fields (isCollection, tagName, mechanism) are copied from the EnumTypeAttribute associated with the Type of the Enum. The most important part is CSS Selector stored by private string _css is composed by "tagName, mechanism" and the unque "string valueString" associated with the Enum entry that can be defined explicitly or not.
The composition happens in two steps. First, the EnumMemberAttribute of an Enum entry shall be retrieved and cached:
public static readonly char[] EnumValueSplitters = { ' ', '=' };
public static readonly Dictionary<Enum, EnumMemberAttribute> CachedEnumMemberAttributes = new Dictionary<Enum, EnumMemberAttribute>();
public static string DescriptionOf(EnumTypeAttribute typeAttribute, string enumValue)
{
return string.Format("{0}{1} Locator {2}: {3}", typeAttribute.IsCollection ? "All" : "",
typeAttribute.TagName, typeAttribute.Mechanism, enumValue);
}
public static EnumMemberAttribute EnumMemberAttributeOf(Enum theEnum)
{
if (CachedEnumMemberAttributes.ContainsKey(theEnum))
return CachedEnumMemberAttributes[theEnum];
Type enumType = theEnum.GetType();
EnumTypeAttribute typeAttribute = EnumTypeAttribute.TypeAttributeOf(enumType);
EnumMemberAttribute memberAttribute = null;
var fi = enumType.GetField(theEnum.ToString());
var usageAttributes = GetCustomAttributes(fi, typeof(EnumMemberAttribute), false);
int attributesCount = usageAttributes.Count();
string enumValue = null;
if (attributesCount == 0)
{
enumValue = theEnum.DefaultValue();
memberAttribute = new EnumMemberAttribute(enumValue, false, DescriptionOf(typeAttribute, enumValue));
}
else if (attributesCount == 1)
{
EnumMemberAttribute attr = (EnumMemberAttribute)usageAttributes[0];
enumValue = attr.Value ?? theEnum.DefaultValue();
memberAttribute = new EnumMemberAttribute(enumValue,
attr.IsFragmentLocator, attr.Description ?? DescriptionOf(typeAttribute, enumValue));
}
if (memberAttribute == null)
throw new Exception("Unexpected situation when memberAttribute is null.");
string css = CssSelectorOf(typeAttribute, memberAttribute, enumValue);
memberAttribute = new EnumMemberAttribute(memberAttribute.Value, memberAttribute.IsFragmentLocator, memberAttribute.Description, css,
typeAttribute.Mechanism, typeAttribute.IsCollection, typeAttribute.TagName);
CachedEnumMemberAttributes.Add(theEnum, memberAttribute);
return memberAttribute;
}
If there is no EnumMemberAttribute defined explicitly for an Enum member, then usually the string format of the member is treated as the enumValue, otherwise the custom specified items including enumValue would be used in CssSelectorOf() to get the CSS Selector. Only after that, a EnumMemberAttribute new will be constructed and cached for later reference to avoid impact on the performance.
As mentioned in previous section, String.Format() is used twice to get the expected CSS selector in the CssSelectorOf() except when Mechnisms.ByCustom is used when the whole CSS selector is user defined.
public static string CssSelectorOf(EnumTypeAttribute typeAttribute, EnumMemberAttribute memberAttribute, string enumIdValue)
{
HtmlTagName tagName = typeAttribute.TagName;
string tagLocator = tagName.Value();
Mechanisms mechanism = typeAttribute.Mechanism;
string mechanismFormat = typeAttribute.Mechanism.Value();
string css;
switch (mechanism)
{
case Mechanisms.ById:
{
string byId = string.Format(mechanismFormat, string.Empty, enumIdValue);
css = string.Format(tagLocator, string.Empty, byId);
break;
}
case Mechanisms.ByClass:
css = string.Format(mechanismFormat, string.Empty, enumIdValue);
css = string.Format(tagLocator, string.Empty, css);
break;
case Mechanisms.ByCustom:
default:
css = enumIdValue;
break;
}
return css.Trim();
}
As a result, taken ButtonById as example: "{0}#{1}" is associated with Mechanisms.ById, and "{0} button{1}, {0} input[type=button]{1}, {0} input.btn{1}, {0} input.button{1}, {0} div[role=button]{1}", which represents 5 common expressions of Button element in HTML, is associated with HtmlTagName.Button.
Because there is no explicit EnumMemberAttribute defined for "ButtonById.button1", its enumValue is thus "button1" and as can be validated by EnumMemberAttributeTests.cs:
[Test]
public void EnumMemberAttribute_NoExplictDefinition1_AsExpected()
{
EnumMemberAttribute memberAttribute = EnumMemberAttribute.EnumMemberAttributeOf(ButtonById.button1);
Console.WriteLine("CSS: " + memberAttribute.Css);
Assert.IsTrue(memberAttribute.Css.Contains("button#button1"));
Assert.IsNotNullOrEmpty(memberAttribute.Description);
}
The CSS Selector, just as the output shown, is:
CSS: button#button1, input[type=button]#button1, input.btn#button1, input.button#button1, div[role=button]#button1
In case of "Fragment.ButtonById.button1", its value is defined in codes as
[EnumMember("otherId")]
So its CSS Selector, as validated by following codes:
[Test]
public void EnumMemberAttribute_WithValueDefined_AsExpected()
{
EnumMemberAttribute memberAttribute = EnumMemberAttribute.EnumMemberAttributeOf(Fragment.ButtonById.button1);
Console.WriteLine("CSS: " + memberAttribute.Css);
Assert.IsTrue(memberAttribute.Css.Contains("button#otherId"));
}
is:
CSS: button#otherId, input[type=button]#otherId, input.btn#otherId, input.button#otherId, div[role=button]#otherId
In this way, only the keyword differentiates one element from another need to be specified in hand. Due to the strong typed feature of Enum, only need to parse it once and stored in a single cached dictionary without worrying about performance.
How to use the codes
There are two projects uploaded with this article: AdvancedEnum.zip and AdvancedEnumUnitTest.zip contain the sample library project and its test project respectively.
The conception and practice discussed above can be found from the AdvancedEnum project, and running the NUnit testing project can validate it can work as expected.
Points of Interest
As a step to introduce the WebDriver based Automation Testing framework, some usage of the C# Enum is discussed here and here are some advantages achieved by using Enum:
- As a special kind of class, the strongly typed feature of Enum means we do not need to worry about the name confliction when string is used instead.
- The method extension on class can be applied on Enum to bring a lot of convenience means to use Enums.
- The static Dictionary of the Enum or any attributes can remove the concerns about any complex operation behind of the Enum.
- The attributes can be applied on either type or member of the Enum, to perform some sophisticated calculation and associate any kinds of information with them.
- In another word, combined with attribute, one Enum entry can convey a full set of information related with one object. As the example discussed above, the EnumMemberAttribute can be used to parse and store the type of the element, mechnism chosen to locate it, and calculate the CSS locator with any enumValue defined explicitly or implicitly.
- As my following articles would demonstrate, because Enum is of limited members of limited types, the Intellisense of VS can alleviate us from inputing every characters and typos.
To demonstrate the succinctness of using Enum in this way, a medium-sized sample class is listed as below:
public class MissionLogisticsFragment : Fragment
{
public enum SectionById
{
[EnumMember("mission-logistics", true)]
mission_logistics
}
public enum CheckboxById
{
crewmixcbx1,
crewmixcbx2,
crewmixcbx3,
crewmixcbx4,
crewmixcbx5,
crewmixcbx6,
crewmixcbx7,
crewmixcbx8,
crewmixcbx9,
crewmixcbx10,
crewmixcbx11,
crewmixcbx12,
crewmixcbx13,
crewmixcbx14,
crewmixcbx15,
crewmixcbx16,
crewmixcbx17
}
public enum SelectById
{
preferredPrincipalPlatform
}
public enum TextById
{
paramedicVacisNumber
}
}
The 38-lines class contains all information needed for constructing CSS Selector of 20 elements as you can guess from the context.
Though focused on a specific application scenario, the same technique demonstrated here can be used in anywhere else to achieve same convenience.
History
18 May 2014: Initial version drafted to cover the usage of Method Extension, Attribute on Enum Type and Enum Members.