65.9K
CodeProject 正在变化。 阅读更多。
Home

高级枚举用法及示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (18投票s)

2014年6月18日

CPOL

13分钟阅读

viewsIcon

43416

downloadIcon

749

利用枚举的类型名和成员定义,以及枚举的扩展方法。

引言

本文介绍了 C# 中枚举使用的一些高级功能,展示了如何定义和提取丰富的 Enum 类型信息。

有关其应用的更多详细信息,请参阅 TERESA: a compact WebDriver Enabler

背景

我之前在一些项目中使用了这里提出的技巧,以利用枚举的强类型特性实现简洁的代码,但奇怪的是,我还没有找到类似的讨论。作为我正在进行的 WebDriver 自动化测试框架的关键概念,我定义了各种枚举类型来方便地存储 CSS 定位器信息。

通常,这些枚举类型的名称由两部分组成:HTML 标签名和 CSS 定位机制。

在本文中,我将使用一些提取的代码来演示如何通过类型名、带特性的成员和扩展方法来传递信息。最后,我将总结枚举相对于字符串的优势。

关于枚举

请参考 这里。枚举是一组命名常量,其底层类型是任何整型。 尽管枚举的声明语法看起来并不面向对象,但事实是,在 .NET Framework 中,枚举类型是具有自己明确定义的继承层次结构的常规类型。枚举类型不允许拥有暴露自身方法、属性和事件的对象模型。然而,对象模型存在于所有枚举的基类 System.Enum 上。

虽然不如 JAVA 中的同行灵活,但强类型的 .NET 枚举仍然是有价值的,作为一组共享一些共同点和相同基类 System.Enum 的常量值集合。

关于 CSS 选择器

使用 Selenium IDE,可以通过多种方式定位 HTML 文档中的元素或元素组,并且强烈推荐使用 CSS 选择器而不是 XPath。

可以在 这里 找到常用 CSS 选择器的便捷摘要。

根据 这篇文章,从高到低效率的 CSS 选择器顺序如下:

  1. ID,例如 #header
  2. 类,例如 .promo
  3. 类型,例如 div
  4. 相邻兄弟,例如 h2 + p
  5. 子元素,例如 li > ul
  6. 后代,例如 ul a
  7. 通用,即 *
  8. 属性,例如 [type="text"]
  9. 伪类/伪元素,例如 a:hover

虽然不像 XPath 那样有方便的方法通过文本定位元素,但在获取可能的候选元素集合后,可以通过 LINQ 查询来支持。此外,按文本定位元素显然是最低效的方式,可以通过使用兄弟/父关系或其他机制(如 nth-child(n) 和 nth-of-type(n))来避免。考虑到其效率和可读性,我的框架基于 CSS 选择器。

关于 WebDriver

Selenium WebDriver 是用于自动化测试 Web 应用程序的工具,特别是用于验证它们是否按预期工作。它旨在提供一个友好且易于探索和理解的 API,这将有助于使您的测试更易于阅读和维护。 一个简单的入门方法是这个例子,它在 Google 上搜索“Cheese”这个词,然后将结果页面的标题输出到控制台。

using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;

// Requires reference to WebDriver.Support.dll
using OpenQA.Selenium.Support.UI;

class GoogleSuggest
{
    static void Main(string[] args)
    {
        // Create a new instance of the Firefox driver.

        // Notice that the remainder of the code relies on the interface, 
        // not the implementation.

        // Further note that other drivers (InternetExplorerDriver,
        // ChromeDriver, etc.) will require further configuration 
        // before this example will work. See the wiki pages for the
        // individual drivers at http://code.google.com/p/selenium/wiki
        // for further information.
        IWebDriver driver = new FirefoxDriver();

        //Notice navigation is slightly different than the Java version
        //This is because 'get' is a keyword in C#
        driver.Navigate().GoToUrl("http://www.google.com/");

        // Find the text input element by its name
        IWebElement query = driver.FindElement(By.Name("q"));

        // Enter something to search for
        query.SendKeys("Cheese");

        // Now submit the form. WebDriver will find the form for us from the element
        query.Submit();

        // Google's search is rendered dynamically with JavaScript.
        // Wait for the page to load, timeout after 10 seconds
        WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
        wait.Until((d) => { return d.Title.ToLower().StartsWith("cheese"); });

        // Should see: "Cheese - Google Search"
        System.Console.WriteLine("Page title is: " + driver.Title);

        //Close the browser
        driver.Quit();
    }
}

本文仅关注此示例中的两行代码,即

IWebElement query = driver.FindElement(By.Name("q"));
query.SendKeys("Cheese");

FindElement() 和 SendKeys() 的代码含义不言而喻,WebDriver 将定位名为“q”的查询元素并向其中输入“Cheese”。

然而,Selenium 提出的这种常见做法存在一些不便之处。

  • 元素的定位器(在此示例中,是搜索一个名为“q”的文本字段)与代码耦合。
    • 如果网页发生更改,并且查询字段需要通过“ID”来定位,或者“q”的“Name”更改为“p”,那么就需要更新多个地方。
    • 因此,C# 开发人员和网页设计师之间无法划分工作。
    • 虽然可以使用常量字符串(例如“query”)来存储“q”,但其背后的机制难以包含,并且当可能存在多个同名元素时会非常混乱。
  • 查询元素的类型含糊不清。
    • 首先,这意味着驱动程序必须查询页面中的所有元素,然后才能匹配其名为“q”的名称,显然,定位元素不如先将其指定为“textarea”或“input”高效。
    • 其次,扩展 IWebElement 以为某些特定元素引入更多功能并不容易。例如,正如我在上一篇文章《高效操作 WebDriver 元素》中所述,table 元素应该与文本或按钮字段具有截然不同的行为。当然,这只有在元素首先被识别为特定类型后才能发生。
    • 最后,如果不阅读网页源代码,就很难立即理解代码是否正确。例如,如果名为“q”的元素实际上是一个按钮,那么代码就可以运行,但无法提前看到自动化测试没有按预期运行。
  • 仍然存在过多的冗余和易出错。在这个简单的示例中并不明显,但当涉及一个包含成百上千个元素的庞大项目时,不断输入“FindElement(By.Something("somekey"))”和“SendKeys()”是繁琐且不必要的。

我的期望

为了解决这些问题,可以采取一些措施来提高自动化测试设计的生产力。
  1. 将元素定位器与可执行代码分离。也就是说,元素定位器的机制和关键字与 [test] 部分下的代码分开定义。因此,如果网页发生更改,只需相应地更新定位器。
  2. 尽早识别元素的类型。结果,WebDriver 可以筛掉标签名不相关的元素,并且这种识别意味着在将相关函数封装到不同类型的元素后,我们可以以不同的方式操作元素。
  3. 在枚举类型名称中识别用于定位元素的机制。
  4. 通过为不同的枚举条目分配不同的 Enum.Value(),将具有相同机制和相同元素类型的定位器分组在一起。这在典型的网页中尤其有用,例如,许多链接/按钮/文本与其他元素略有不同。
  5. 可以通过索引器简化测试用例,其原型是“string this[Enum enumValue]”,将在未来的文章中介绍。虽然我的框架中尚未实现,但通过这些统一的 getter 和 setter 方法,可以将测试操作定义为独立的属性文件,该文件可以由 UI 设计师起草并在执行测试脚本时动态加载。

因此,相关元素的定位器可以定义在一个类似“GooglePageObject.cs”的文件中,如下所示:

public class GooglePageObject{
    public TextByName{
        //q, //Using the keyword as the member name without explicit attribute
        [EnumMember("q")]
        query
    }
}

 在测试代码中,代码如下:

somepage[GooglePageObject.query] = "Cheese";
//somepage[GooglePageObject.q] = "Cheese"; //if "q" instead of "query" is defined
以下部分用于解释如何使用枚举定义和访问定位器。

利用枚举的潜力

枚举的扩展方法

枚举类型都继承自 System.Enum,它实际上是一个类,因此我们可以扩展 Enum 来实现一些便利,例如“EnumExtension.cs”附件中的 Value()。

    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];
        }

    ...

    }

然后,在同一个命名空间中,我们可以这样使用它:

public enum SomeEnumType { entry1, entry2};

string enumValue = SomeEnumType.entry1.Value();

值得注意的是,有一个 Dictionary<Enum, string> 来缓存任何枚举条目的值,以考虑效率和一致性。由于枚举的强类型特性,可能存在多个具有相同枚举类型名和成员名但在不同命名空间中的条目,就像“Identify Both Element Types and Locator Mechanisms in EnumTypeAttribute”部分中的两个“ButtonById”一样;它们可以具有不同的值,这可以在 AdvancedEnumUnitTest 中的“EnumMemberAttributeTests.cs”中进行验证。

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

为枚举类型和成员分配属性

属性提供了一种将声明性信息与 C# 代码(类型、方法、属性等)关联的强大方法。一旦与程序实体关联,属性就可以在运行时进行查询并以任意方式使用。 

与字符串不同, Enum 本质上是一种特殊的类,因此我们可以将属性关联到它的类型和成员条目。

与枚举类型关联的 EnumTypeAttribute 属性

[AttributeUsage(AttributeTargets.Enum, Inherited = false, AllowMultiple = false), Serializable]
public class EnumTypeAttribute : Attribute
{ //codes }

与枚举成员条目关联的 EnumMemberAttribute 属性

[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false), Serializable]
public class EnumMemberAttribute : Attribute
{ //codes }

在 EnumTypeAttribute 中识别元素类型和定位机制

元素的类型被定义为 Enum 类型的 HtmlTagName,下面是从中提取的框架的简化版本:

    public enum HtmlTagName
    {
        Unknown,
        //Common ElementWrapper types
        Button,
        Link,
        Text,
        Radio
    }

获取 CSS 选择器的机制被定义为另一个 Enum 类型 Mechanism,下面是从中提取的框架的简化版本:

    public enum Mechanisms
    {
        //Common Mechanisms, in order of efficiency preference
        ById              //Using SectionByAttr as finding mechanism
        ,
        ByClass         //Using Classname for finding
        ,
        ByCustom        //All css text definded by the Enum.ByValue()
    }

预期的有效类型名称由 HtmlTagName 的任何条目(除了“Unknown”)和 Mechanisms 的任何条目组成,因此即使在这个简化版本中,也有 4(Button、Link、Text 和 Radio)* 3(ById、ByClass、ByCustom)= 12 种组合来定义如何检索 CSS 选择器。一些示例可以在附件中的 TestEnums.cs 中找到。

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
        }
    }
}

有效枚举类型名的构成是:TagName (+ 可选的“All”) + Mechanisms。

这里出现的有效枚举类型的含义不言而喻:

  • ButtonById:通过 ID 定位按钮元素。
  • TextAllByClass:通过类名定位文本输入或 textarea 的集合。
  • RadioByCustom:通过自定义 CSS 选择器字符串定位单选按钮集合。
  • ButtonByClass:通过类名定位按钮元素。

有几点需要注意:

  • 类型名本身包含足够的信息来生成相应的 CSS 选择器,详细信息将在...讨论。
  • 共享相同标签名和相似定义的类似元素应归为同一枚举类型,这样可以节省大量输入。另一方面,要引用枚举类型的条目,类型名始终是强制性的,因此为开发人员提供了足够的线索来预见元素类型以及 CSS 定位符的构成方式。
  • 有两个同名的枚举类型“ButtonById”:一个在基本命名空间中,另一个在 Fragment 类中。由于枚举是强类型的,因此可以引用这两个类型:第一个类型称为“ButtonById”,第二个类型称为“Fragment.ButtonById”。
  • “ByCustom”机制意味着其成员的 CSS 选择器应始终显式定义。
  • “TextAllByClass”中出现的可选“All”表示有多个文本字段共享同一个类。

用于分析枚举类型名以获取其含义的函数包含在“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);
}

通常,它尝试将类型名与 HtmlTagName 和 Mechanisms 的项进行匹配。同样,使用缓存来避免多次解析相同的类型名。但是,只使用枚举类型名作为字符串作为键,假设它们严格按照预期格式构成。

为了通过 HtmlTagName 和 Mechanisms 生成 CSS 选择器,它们中的每个项都预先关联了一些格式字符串。

static EnumTypeAttribute()
{
    #region Registering Default CSS construction formats needed by different Mechanism
    //E#myid:    Matches any E element with ID equal to "myid".
    EnumExtension.RegisterEnumValue(Mechanisms.ById, "{0}#{1}");
    //DIV.warning    Language specific. (In SinglePage, the same as DIV[class~="warning"].)
    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[] { ' ', ',' });
}

例如,“{0}#{1}”与 Mechanisms.ById 相关联,而“ {0} button{1}, {0} input[type=button]{1}, {0} input.btn{1}, {0} input.button{1}, {0} div[role=button]{1}”(代表 HTML 中按钮元素的 5 种常见表达式)与 HtmlTagName.Button 相关联。然后,使用 String.Format() 两次,我们可以获得预期的 CSS 选择器,如下节将讨论的。

使用 EnumMemberAttribute 识别元素并生成 CSS 选择器

EnumMemberAttribute,意味着传递来自 Enum Typename 和元素标识符的信息,有三个构造函数和多个字段来存储与元素相关的各种信息。

[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;
    }
}

最后三个字段(isCollection, tagName, mechanism)是从与 Enum 类型关联的 EnumTypeAttribute 复制的。最重要部分是由 private string _css 存储的 CSS 选择器,它由 "tagName, mechanism" 以及可以显式或隐式定义的枚举条目相关的唯一 "string valueString" 组成。

组合发生在两个步骤中。首先,检索并缓存枚举条目的 EnumMemberAttribute。

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

        //Get the Type Attribute: IsCollection, Mechanism and HtmlTag
        EnumTypeAttribute typeAttribute = EnumTypeAttribute.TypeAttributeOf(enumType);
        EnumMemberAttribute memberAttribute = null;

        //Try to get the EnumMemberAttribute defined in codes, especially the Meaning of theEnum
        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();
            //if (enumValue != theEnum.DoString())
            //    theEnum.ChangeValue(enumValue);

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

如果没有为枚举成员显式定义 EnumMemberAttribute,那么通常将成员的字符串格式视为 enumValue;否则,将在 CssSelectorOf() 中使用自定义指定的项(包括 enumValue)来获取 CSS 选择器。只有在此之后,才会构造并缓存一个 EnumMemberAttribute 新实例以供以后参考,以避免影响性能。

如前一节所述,String.Format() 在 CssSelectorOf() 中使用了两次来获取预期的 CSS 选择器,除非使用 Mechnisms.ByCustom 时,整个 CSS 选择器由用户定义。

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

因此,以 ButtonById 为例:“{0}#{1}”与 Mechanisms.ById 相关联,而“ {0} button{1}, {0} input[type=button]{1}, {0} input.btn{1}, {0} input.button{1}, {0} div[role=button]{1}”(代表 HTML 中按钮元素的 5 种常见表达式)与 HtmlTagName.Button 相关联。

由于“ButtonById.button1”没有显式定义 EnumMemberAttribute,因此其 enumValue 为“button1”,如 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);
}

CSS 选择器,正如输出所示,是:

CSS: button#button1,  input[type=button]#button1,  input.btn#button1,  input.button#button1,  div[role=button]#button1

对于“Fragment.ButtonById.button1”,其值在代码中定义为:

[EnumMember("otherId")]

因此,其 CSS 选择器,如以下代码验证所示:

[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

通过这种方式,只需手动指定区分一个元素的关键字即可。由于枚举的强类型特性,只需解析一次并存储在单个缓存字典中,无需担心性能。

如何使用代码

本文上传了两个项目:AdvancedEnum.zip 和 AdvancedEnumUnitTest.zip 分别包含示例库项目及其测试项目。

上面讨论的概念和实践可以在 AdvancedEnum 项目中找到,运行 NUnit 测试项目可以验证其是否按预期工作。

关注点

作为引入基于 WebDriver 的自动化测试框架的步骤,本文讨论了一些 C# 枚举的用法,并列出了使用枚举获得的一些优点:

  • 作为一种特殊的类,枚举的强类型特性意味着我们不必担心使用字符串时发生的名称冲突。
  • 类的方法扩展可以应用于枚举,以带来许多便利的枚举使用方式。
  • 枚举或任何属性的静态 Dictionary 可以消除对枚举背后复杂操作的担忧。
  • 属性可以应用于枚举的类型或成员,以执行一些复杂的计算并将任何类型的信息与它们关联起来。
  • 换句话说,结合属性,一个枚举条目可以传递与一个对象相关的一整套信息。正如上面讨论的示例,EnumMemberAttribute 可用于解析和存储元素的类型、用于定位它的机制,并计算 CSS 定位符,其中包含任何显式或隐式定义的 enumValue。
  • 正如我接下来的文章将演示的,由于枚举的成员类型有限,VS 的智能感知功能可以帮助我们避免输入所有字符和拼写错误。

为了演示使用枚举的简洁性,下面列出了一个中等大小的示例类:

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
    }
}

这个 38 行的类包含了构建 20 个元素 CSS 选择器所需的所有信息,正如您从上下文推断的那样。

虽然专注于特定的应用场景,但这里演示的相同技术可以在任何其他地方用于实现相同的便利性。

历史

2014 年 5 月 18 日:初稿完成,涵盖了方法扩展、枚举类型和枚举成员上的属性的使用。

© . All rights reserved.