我正在尝试编写一个应用程序的自动化测试,该应用程序基本上是将自定义消息格式转换为XML消息并将其发送到另一端。我已经有了一组很好的输入/输出消息对,所以我所需要做的就是将输入消息发送进来,并侦听XML消息从另一端传出来。

当需要将实际输出与预期输出进行比较时,我遇到了一些问题。我的第一个想法是对预期消息和实际消息进行字符串比较。这并不能很好地工作,因为我们拥有的示例数据的格式并不总是一致的,而且XML名称空间经常使用不同的别名(有时根本不使用名称空间)。

我知道我可以解析两个字符串,然后遍历每个元素并自己进行比较,这不会太难做到,但我感觉有更好的方法或我可以利用的库。

所以,归结起来,问题是:

给定两个Java字符串,都包含有效的XML,你将如何决定他们是否在语义上等价?如果你有办法确定区别是什么,那就更好了。


当前回答

这将比较完整的字符串xml(在此过程中重新格式化它们)。它使您很容易使用IDE (IntelliJ, Eclipse),因为您只需单击并直观地看到XML文件中的差异。

import org.apache.xml.security.c14n.CanonicalizationException;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.c14n.InvalidCanonicalizerException;
import org.w3c.dom.Element;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.io.StringReader;

import static org.apache.xml.security.Init.init;
import static org.junit.Assert.assertEquals;

public class XmlUtils {
    static {
        init();
    }

    public static String toCanonicalXml(String xml) throws InvalidCanonicalizerException, ParserConfigurationException, SAXException, CanonicalizationException, IOException {
        Canonicalizer canon = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS);
        byte canonXmlBytes[] = canon.canonicalize(xml.getBytes());
        return new String(canonXmlBytes);
    }

    public static String prettyFormat(String input) throws TransformerException, ParserConfigurationException, IOException, SAXException, InstantiationException, IllegalAccessException, ClassNotFoundException {
        InputSource src = new InputSource(new StringReader(input));
        Element document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src).getDocumentElement();
        Boolean keepDeclaration = input.startsWith("<?xml");
        DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
        DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
        LSSerializer writer = impl.createLSSerializer();
        writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
        writer.getDomConfig().setParameter("xml-declaration", keepDeclaration);
        return writer.writeToString(document);
    }

    public static void assertXMLEqual(String expected, String actual) throws ParserConfigurationException, IOException, SAXException, CanonicalizationException, InvalidCanonicalizerException, TransformerException, IllegalAccessException, ClassNotFoundException, InstantiationException {
        String canonicalExpected = prettyFormat(toCanonicalXml(expected));
        String canonicalActual = prettyFormat(toCanonicalXml(actual));
        assertEquals(canonicalExpected, canonicalActual);
    }
}

与XmlUnit相比,我更喜欢它,因为客户机代码(测试代码)更干净。

其他回答

下面将使用标准JDK库检查文档是否相等。

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setCoalescing(true);
dbf.setIgnoringElementContentWhitespace(true);
dbf.setIgnoringComments(true);
DocumentBuilder db = dbf.newDocumentBuilder();

Document doc1 = db.parse(new File("file1.xml"));
doc1.normalizeDocument();

Document doc2 = db.parse(new File("file2.xml"));
doc2.normalizeDocument();

Assert.assertTrue(doc1.isEqualNode(doc2));

Normalize()用于确保没有循环(技术上不会有任何循环)

上面的代码将要求元素中的空格相同,因为它保留并计算它。Java附带的标准XML解析器不允许您设置一个特性来提供规范版本或理解XML:space(如果这将成为一个问题,那么您可能需要替换XML解析器,如xerces或使用JDOM)。

下面的代码为我工作

String xml1 = ...
String xml2 = ...
XMLUnit.setIgnoreWhitespace(true);
XMLUnit.setIgnoreAttributeOrder(true);
XMLAssert.assertXMLEqual(actualxml, xmlInDb);

既然您说“语义等效”,我假设您的意思是您想做的不仅仅是字面上验证xml输出是(string)等于,并且您想要类似于

<foo>这里有些东西</foo></code>

and

<foo>这里有些东西</foo></code>

阅读时一定要对等。最终,这将关系到您如何定义“语义等效”,无论您从哪个对象重构消息。只需从消息构建该对象,并使用自定义equals()定义您要查找的内容。

这将比较完整的字符串xml(在此过程中重新格式化它们)。它使您很容易使用IDE (IntelliJ, Eclipse),因为您只需单击并直观地看到XML文件中的差异。

import org.apache.xml.security.c14n.CanonicalizationException;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.c14n.InvalidCanonicalizerException;
import org.w3c.dom.Element;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.io.StringReader;

import static org.apache.xml.security.Init.init;
import static org.junit.Assert.assertEquals;

public class XmlUtils {
    static {
        init();
    }

    public static String toCanonicalXml(String xml) throws InvalidCanonicalizerException, ParserConfigurationException, SAXException, CanonicalizationException, IOException {
        Canonicalizer canon = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS);
        byte canonXmlBytes[] = canon.canonicalize(xml.getBytes());
        return new String(canonXmlBytes);
    }

    public static String prettyFormat(String input) throws TransformerException, ParserConfigurationException, IOException, SAXException, InstantiationException, IllegalAccessException, ClassNotFoundException {
        InputSource src = new InputSource(new StringReader(input));
        Element document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src).getDocumentElement();
        Boolean keepDeclaration = input.startsWith("<?xml");
        DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
        DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
        LSSerializer writer = impl.createLSSerializer();
        writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
        writer.getDomConfig().setParameter("xml-declaration", keepDeclaration);
        return writer.writeToString(document);
    }

    public static void assertXMLEqual(String expected, String actual) throws ParserConfigurationException, IOException, SAXException, CanonicalizationException, InvalidCanonicalizerException, TransformerException, IllegalAccessException, ClassNotFoundException, InstantiationException {
        String canonicalExpected = prettyFormat(toCanonicalXml(expected));
        String canonicalActual = prettyFormat(toCanonicalXml(actual));
        assertEquals(canonicalExpected, canonicalActual);
    }
}

与XmlUnit相比,我更喜欢它,因为客户机代码(测试代码)更干净。

使用XMLUnit 2.x

在pom.xml中

<dependency>
    <groupId>org.xmlunit</groupId>
    <artifactId>xmlunit-assertj3</artifactId>
    <version>2.9.0</version>
</dependency>

测试实现(使用junit 5):

import org.junit.jupiter.api.Test;
import org.xmlunit.assertj3.XmlAssert;

public class FooTest {

    @Test
    public void compareXml() {
        //
        String xmlContentA = "<foo></foo>";
        String xmlContentB = "<foo></foo>";
        //
        XmlAssert.assertThat(xmlContentA).and(xmlContentB).areSimilar();
    }
}

其他方法:aresame (), areNotIdentical(), areNotSimilar()

更多细节(assertThat(~).and(~)的配置和示例)请参见本文档页。

XMLUnit还有一个DifferenceEvaluator(在其他特性中),用于进行更精确的比较。

用的网站