Подтвердить что ты не робот

Лучший способ сравнить объекты XElement

в unit test Я сравниваю объект XElement с тем, который я ожидаю. Метод, который я использую, заключается в вызове .ToString() объекта XElement и сопоставления его с строковым строковым значением. Этот метод оказался довольно неудобным, так как я всегда должен обратить внимание на форматирование в строке.

Я проверил метод XElement.DeepEquals(), но по какой-либо причине он не помогает.

Есть ли у кого-нибудь идея, какой лучший метод я должен использовать?

4b9b3361

Ответ 1

Я нашел эту отличную статью полезной. Он содержит образец кода, который реализует альтернативу 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))
            );
        }
    }
}

Ответ 2

Я начал с того же пути, что и @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));
}

Даже если есть несколько элементов и атрибутов, это управляемо и непротиворечиво.

Ответ 3

У меня возникла проблема сравнения XElements для равенства, где у одного из элементов были дочерние узлы, где теги, закрывающие закрытие, а другие - открытые и закрытые теги, например. [blah/] vs [blah] [/blah]

Функция глубоких равных, конечно же, сообщала о том, что они разные, поэтому мне нужна нормализованная функция. Я закончил тем, что использовал вариант того, что опубликовано в этом блоге ( "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);
}

Ответ 4

В зависимости от того, что вы тестируете. Вам нужно убедиться, что XML равен или эквивалентен.

Я подозреваю последнее, и в этом случае вы должны запрашивать xelement с помощью xlinq и утверждать, что он имеет необходимые элементы и атрибуты.

В конце дня происходит то, что требуется. Например

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

и

<element att='zzz' />

может быть эквивалентным, если вы не заботитесь о <sub /> or att

Ответ 5

Следующий шаг, который может помочь: нормализация, которая избавляется от ЛЮБОГО упорядочения. Иногда порядок элементов вообще не имеет значения (подумайте о коллекциях, а не о списках или массивах).

Это основано на предыдущем (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.