在一个单元测试中,我正在比较一个XElement
对象与我期望的一个。我使用的方法是在XElement
对象上调用.ToString()
方法,并将其与一个硬编码的字符串值进行比较。这种方法非常不方便,因为我总是需要注意字符串的格式。
我查看了XElement.DeepEquals()
方法,但出于某种原因它并没有帮助。
有人有什么好的方法吗?
在一个单元测试中,我正在比较一个XElement
对象与我期望的一个。我使用的方法是在XElement
对象上调用.ToString()
方法,并将其与一个硬编码的字符串值进行比较。这种方法非常不方便,因为我总是需要注意字符串的格式。
我查看了XElement.DeepEquals()
方法,但出于某种原因它并没有帮助。
有人有什么好的方法吗?
我发现这篇优秀的文章很有用。它包含一个代码示例,实现了一种替代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("<a />"),XDocument.Parse("<a></a>"));
返回 false
。请参见此处的更多评论:https://dev59.com/fYDba4cB1Zd3GeqPFYdo#24156847 - JohnLBevan我走了和 @llasarov 一样的路线,但也不喜欢使用字符串。我在这里发现了 XElement.DeepEquals(),所以找到了这个问题帮助了我。
如果你的测试返回一个庞大的 XML 结构,我可以看出这可能很困难,但在我看来,这不应该这样做——测试应该尽可能检查小型结构。
假设你有一个方法,期望返回一个类似于 <Test Sample="Value" />
的元素。你可以使用 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
是我所见过或使用过的最好的节点单元测试相等性的方法。 - atconway我有一个比较XElements是否相等的问题,其中一个元素具有自关闭标记的子节点,而另一个元素具有开放和关闭标记,例如[blah /] vs [blah] [/ blah]。
深度相等函数当然报告它们是不同的,所以我需要一个规范化函数。最终我使用了此博客(由“marianor”发布)中发布的变体:
http://weblogs.asp.net/marianor/archive/2009/01/02/easy-way-to-compare-two-xmls-for-equality.aspx
一个小修改是在规范化后使用深度相等函数(而不是字符串比较),而且我添加了逻辑来将包含空文本的元素视为与空元素相同(以解决上述问题)。结果如下。
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);
}
Assert.Equal
与 XNode.EqualityComparer
:var expected = new XElement("Sample", new XAttribute("name", "test"));
var result = systemUnderTest.DoSomething();
Assert.Equal(expected, result, XNode.EqualityComparer);
这种方法的优点是,如果测试失败,它会在错误消息中打印出XML元素,以便您可以看到它们之间的区别。
对于单元测试,我认为最简单的方法是让XElement解析您预期的字符串。
string expected = "<some XML>";
XElement result = systemUnderTest.DoSomething();
Assert.Equal(XElement.Parse(expected).ToString(), result.ToString());
一个可能有帮助的下一步是进行规范化,以消除任何排序。有时元素的顺序根本不重要(考虑集合而非列表或数组)。
这个方法基于之前的方法(由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 );
}
Enumerable.Concatenate扩展方法与string.Join方法相同。
这取决于你要测试什么。你需要验证XML是否相等或等效吗?
我猜是后者,在这种情况下,你应该使用xlinq查询xelement,并断言它具有所需的元素和属性。
归根结底,这取决于所需的内容。例如:
<element att='xxxx'>
<sub />
</element>
并且
<element att='zzz' />
如果您不关心<sub />或att
,则可能是等效的。