【问题标题】:Best way to compare XElement objects比较 XElement 对象的最佳方法
【发布时间】:2011-09-06 10:05:09
【问题描述】:

在单元测试中,我将XElement 对象与我期望的对象进行比较。我使用的方法是在XElement 对象上调用.ToString() 并将其与硬编码的字符串值进行比较。结果证明这种方法很不舒服,因为我总是要注意字符串中的格式。

我检查了 XElement.DeepEquals() 方法,但由于某种原因它没有帮助。

有人知道我应该使用什么最好的方法吗?

【问题讨论】:

  • DeepEquals 是要走的路。请显示您正在比较的两个 XElement 的字符串表示形式。
  • 确实最好的方法是使用 DeepEqual(XElement.Parse(expectedAsString), actualXElement)!

标签: .net xml linq unit-testing


【解决方案1】:

我发现this excellent article 很有用。它包含一个代码示例,该示例实现了XNode.DeepEquals 的替代方案,该替代方案在比较之前对 XML 树进行规范化,从而使非语义内容变得无关紧要。

为了说明,XNode.DeepEquals 的实现对于这些语义等效的文档返回 false:

XElement root1 = XElement.Parse("<Root a='1' b='2'><Child>1</Child></Root>");
XElement root2 = XElement.Parse("<Root b='2' a='1'><Child>1</Child></Root>");

但是,使用本文中DeepEqualsWithNormalization 的实现,您将获得true 的值,因为属性的顺序并不重要。此实现包含在下面。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

public static class MyExtensions
{
    public static string ToStringAlignAttributes(this XDocument document)
    {
        XmlWriterSettings settings = new XmlWriterSettings();
        settings.Indent = true;
        settings.OmitXmlDeclaration = true;
        settings.NewLineOnAttributes = true;
        StringBuilder stringBuilder = new StringBuilder();
        using (XmlWriter xmlWriter = XmlWriter.Create(stringBuilder, settings))
            document.WriteTo(xmlWriter);
        return stringBuilder.ToString();
    }
}

class Program
{
    private static class Xsi
    {
        public static XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";

        public static XName schemaLocation = xsi + "schemaLocation";
        public static XName noNamespaceSchemaLocation = xsi + "noNamespaceSchemaLocation";
    }

    public static XDocument Normalize(XDocument source, XmlSchemaSet schema)
    {
        bool havePSVI = false;
        // validate, throw errors, add PSVI information
        if (schema != null)
        {
            source.Validate(schema, null, true);
            havePSVI = true;
        }
        return new XDocument(
            source.Declaration,
            source.Nodes().Select(n =>
            {
                // Remove comments, processing instructions, and text nodes that are
                // children of XDocument.  Only white space text nodes are allowed as
                // children of a document, so we can remove all text nodes.
                if (n is XComment || n is XProcessingInstruction || n is XText)
                    return null;
                XElement e = n as XElement;
                if (e != null)
                    return NormalizeElement(e, havePSVI);
                return n;
            }
            )
        );
    }

    public static bool DeepEqualsWithNormalization(XDocument doc1, XDocument doc2,
        XmlSchemaSet schemaSet)
    {
        XDocument d1 = Normalize(doc1, schemaSet);
        XDocument d2 = Normalize(doc2, schemaSet);
        return XNode.DeepEquals(d1, d2);
    }

    private static IEnumerable<XAttribute> NormalizeAttributes(XElement element,
        bool havePSVI)
    {
        return element.Attributes()
                .Where(a => !a.IsNamespaceDeclaration &&
                    a.Name != Xsi.schemaLocation &&
                    a.Name != Xsi.noNamespaceSchemaLocation)
                .OrderBy(a => a.Name.NamespaceName)
                .ThenBy(a => a.Name.LocalName)
                .Select(
                    a =>
                    {
                        if (havePSVI)
                        {
                            var dt = a.GetSchemaInfo().SchemaType.TypeCode;
                            switch (dt)
                            {
                                case XmlTypeCode.Boolean:
                                    return new XAttribute(a.Name, (bool)a);
                                case XmlTypeCode.DateTime:
                                    return new XAttribute(a.Name, (DateTime)a);
                                case XmlTypeCode.Decimal:
                                    return new XAttribute(a.Name, (decimal)a);
                                case XmlTypeCode.Double:
                                    return new XAttribute(a.Name, (double)a);
                                case XmlTypeCode.Float:
                                    return new XAttribute(a.Name, (float)a);
                                case XmlTypeCode.HexBinary:
                                case XmlTypeCode.Language:
                                    return new XAttribute(a.Name,
                                        ((string)a).ToLower());
                            }
                        }
                        return a;
                    }
                );
    }

    private static XNode NormalizeNode(XNode node, bool havePSVI)
    {
        // trim comments and processing instructions from normalized tree
        if (node is XComment || node is XProcessingInstruction)
            return null;
        XElement e = node as XElement;
        if (e != null)
            return NormalizeElement(e, havePSVI);
        // Only thing left is XCData and XText, so clone them
        return node;
    }

    private static XElement NormalizeElement(XElement element, bool havePSVI)
    {
        if (havePSVI)
        {
            var dt = element.GetSchemaInfo();
            switch (dt.SchemaType.TypeCode)
            {
                case XmlTypeCode.Boolean:
                    return new XElement(element.Name,
                        NormalizeAttributes(element, havePSVI),
                        (bool)element);
                case XmlTypeCode.DateTime:
                    return new XElement(element.Name,
                        NormalizeAttributes(element, havePSVI),
                        (DateTime)element);
                case XmlTypeCode.Decimal:
                    return new XElement(element.Name,
                        NormalizeAttributes(element, havePSVI),
                        (decimal)element);
                case XmlTypeCode.Double:
                    return new XElement(element.Name,
                        NormalizeAttributes(element, havePSVI),
                        (double)element);
                case XmlTypeCode.Float:
                    return new XElement(element.Name,
                        NormalizeAttributes(element, havePSVI),
                        (float)element);
                case XmlTypeCode.HexBinary:
                case XmlTypeCode.Language:
                    return new XElement(element.Name,
                        NormalizeAttributes(element, havePSVI),
                        ((string)element).ToLower());
                default:
                    return new XElement(element.Name,
                        NormalizeAttributes(element, havePSVI),
                        element.Nodes().Select(n => NormalizeNode(n, havePSVI))
                    );
            }
        }
        else
        {
            return new XElement(element.Name,
                NormalizeAttributes(element, havePSVI),
                element.Nodes().Select(n => NormalizeNode(n, havePSVI))
            );
        }
    }
}

【讨论】:

  • 注意:XNode.DeepEquals 存在一些问题。例如。 XNode.DeepEquals(XDocument.Parse("&lt;a /&gt;"),XDocument.Parse("&lt;a&gt;&lt;/a&gt;")); 返回false。在此处查看更多 cmets:stackoverflow.com/a/24156847/361842
【解决方案2】:

我从与@llasarov 相同的路径开始,但也不喜欢使用字符串。我在这里发现了 XElement.DeepEquals(),所以找到这个问题对很有帮助。

我可以看到,如果您的测试返回一个庞大的 XML 结构可能会很困难,但在我看来,不应该这样做 - 测试应该检查尽可能小的结构。

假设您有一个方法,您希望返回一个看起来像 &lt;Test Sample="Value" /&gt; 的元素。您可以使用 XElement 和 XAttribute 构造函数非常轻松地构建您的预期值,如下所示:

[TestMethod()]
public void MyXmlMethodTest()
{
    // Use XElement API to build expected element.
    XElement expected = new XElement("Test", new XAttribute("Sample", "Value"));

    // Call the method being tested.
    XElement actual = MyXmlMethod();

    // Assert using XNode.DeepEquals
    Assert.IsTrue(XNode.DeepEquals(expected, actual));
}

即使有少量元素和属性,这也是可管理且一致的。

【讨论】:

  • XNode.DeepEquals 的使用是我见过或用于单元测试节点相等性的最佳方法。
【解决方案3】:

我在比较 XElements 是否相等时遇到问题,其中一个元素具有自关闭标签的子节点,而另一个元素具有打开和关闭标签,例如[blah/] vs [blah][/blah]

deep equals 函数当然会报告它们是不同的,所以我需要一个 normalize 函数。我最终使用了此博客中发布的内容的变体(由“marianor”):

http://weblogs.asp.net/marianor/archive/2009/01/02/easy-way-to-compare-two-xmls-for-equality.aspx

一个小的变化是我在规范化后使用了 deep equals 函数(而不是字符串比较),并且我添加了逻辑来处理包含空文本的元素与空元素相同(以解决上述问题)。结果如下。

private bool CompareXml(string xml)
{
    var a = Normalize(currentElement);
    var b = Normalize(newElement);

    return XElement.DeepEquals(a, b);
}

private static XElement Normalize(XElement element)
{
    if (element.HasElements)
    {
        return new XElement(element.Name, element.Attributes().Where(a => a.Name.Namespace == XNamespace.Xmlns)
                                                                .OrderBy(a => a.Name.ToString()),element.Elements().OrderBy(a => a.Name.ToString())
                                                                .Select(e => Normalize(e)));
    }

    if (element.IsEmpty || string.IsNullOrEmpty(element.Value))
    {
        return new XElement(element.Name, element.Attributes()
            .OrderBy(a => a.Name.ToString()));
    }

    return new XElement(element.Name, element.Attributes()
        .OrderBy(a => a.Name.ToString()), element.Value);
}

【讨论】:

  • 但如果文件中两个元素的顺序被切换,这将不起作用。假设我们要比较 file1 和 file2。在 file1 我们有 A B 在 xml file2 我们有 BA 它们必须相等,因为 xml 中的顺序无关紧要,但此函数返回 false。
【解决方案4】:

在 xunit 中,您可以像这样使用 Assert.EqualsXNode.EqualityComparer

var expected = new XElement("Sample", new XAttribute("name", "test"));
var result = systemUnderTest.DoSomething();

Assert.Equal(expected, result, XNode.EqualityComparer);

这种方法的好处是,如果测试失败,它会打印出错误消息中的xml元素,这样你就可以看到它们的不同之处。

【讨论】:

    【解决方案5】:

    取决于您要测试的内容。是否需要验证 XML 是否相等或等价。

    我怀疑是后者,在这种情况下,您应该使用 xlinq 查询 xelement 并断言它具有所需的元素和属性。

    在一天结束时,它会得出所需的内容。例如

    <element att='xxxx'>
      <sub />
    </element>
    

    <element att='zzz' />
    

    如果你不关心&lt;sub /&gt; or att可能是等价的

    【讨论】:

      【解决方案6】:

      下一步可能会有所帮助:消除任何排序的规范化。有时元素顺序根本不重要(想想集合而不是列表或数组)。

      这个基于前一个(由 RobJohnson 编写),但也根据元素的“内容”对元素进行排序,它使用属性的数量、属性值和 Xml 元素值本身。

      static XElement NormalizeWithoutAnyOrder( XElement element )
      {
          if( element.HasElements )
          {
              return new XElement(
                  element.Name,
                  element.Attributes().OrderBy( a => a.Name.ToString() ),
                  element.Elements()
                      .OrderBy( a => a.Name.ToString() )
                      .Select( e => NormalizeWithoutAnyOrder( e ) )
                      .OrderBy( e => e.Attributes().Count() )
                      .OrderBy( e => e.Attributes()
                                      .Select( a => a.Value )
                                      .Concatenate("\u0001") )
                      .ThenBy( e => e.Value ) );
          }
          if( element.IsEmpty || string.IsNullOrEmpty( element.Value ) )
          {
              return new XElement( element.Name,
                                   element.Attributes()
                                          .OrderBy( a => a.Name.ToString() ) );
          }
          return new XElement( element.Name, 
                               element.Attributes()
                                      .OrderBy( a => a.Name.ToString() ), 
                               element.Value );
      }
      

      IEnumerable.Concatenate 扩展方法与string.Join 方法相同。

      【讨论】:

        【解决方案7】:

        对于单元测试,我发现最简单的方法是让 XElement 也解析您预期的字符串。

        string expected = "<some XML>";
        XElement result = systemUnderTest.DoSomething();
        
        Assert.Equal(XElement.Parse(expected).ToString(), result.ToString());
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-05-14
          • 2012-02-29
          • 2017-11-09
          • 2020-10-23
          • 2015-11-14
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多