in a unit test I'm comparing an XElement
object with the one I expect. The method I use is to invoke .ToString()
on the XElement
object and compare it with a hard-coded string value. This method turned out to be quite uncomfortable since I always have to pay attention on the formatting in the string.
I checked out the XElement.DeepEquals() method but for any reason it doesn't help.
Does anyone has an idea what is the best method I should use?
I found this excellent article useful. It contains a code sample that implements an alternative to XNode.DeepEquals
that normalises the XML trees before comparison which makes non-semantic content irrelevant.
To illustrate, the implementation of XNode.DeepEquals
returns false for these semantically-equivalent documents:
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>");
However, using the implementation of DeepEqualsWithNormalization
from the article, you'll get the value true
because the ordering of attributes is not considered significant. This implementation is included below.
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)) ); } } }
I started down the same path as @llasarov, but also didn't like the use of strings either. I discovered XElement.DeepEquals() here, so finding the question helped me.
I could see that it could be difficult if your test returns a massive XML structure, but in my opinion, this should not be done - the test should check as small a structure as possible.
Say you have a method that you expect to return an element that looks like <Test Sample="Value" />
. You can use the XElement and XAttribute constructors to build your expected value pretty easily, like this:
[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)); }
Even if there are a handful of elements and attributes, this is manageable and consistent.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With