在Java中比较两个XML文档的最佳方法

223
我正在尝试编写自动化测试用例,测试一个应用程序,该程序将定制的消息格式转换为XML消息并将其发送到另一端。我已经准备好了一组良好的输入/输出消息对,所以我只需要将输入消息发送进去,并等待XML消息从另一端出现。
当比较实际输出和期望输出时,我遇到了一些问题。我的第一个想法是仅对期望消息和实际消息进行字符串比较。但这种方法不太可行,因为我们拥有的示例数据格式不一致,并且XML命名空间通常使用不同的别名(有时根本不使用命名空间)。
我知道可以解析两个字符串,然后逐个元素进行比较,虽然这样做不太困难,但我感觉还有更好的方法或者可以利用的库。
综上所述,问题就是:
给定两个包含有效XML的Java字符串,如何确定它们是否在语义上相等?如果你有一种确定差异的方法,那就更好了。
15个回答

2

我需要与主要问题中请求的相同功能。由于不允许使用任何第三方库,因此我基于@Archimedes Trajano的解决方案创建了自己的解决方案。

以下是我的解决方案。

import java.io.ByteArrayInputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.junit.Assert;
import org.w3c.dom.Document;

/**
 * Asserts for asserting XML strings.
 */
public final class AssertXml {

    private AssertXml() {
    }

    private static Pattern NAMESPACE_PATTERN = Pattern.compile("xmlns:(ns\\d+)=\"(.*?)\"");

    /**
     * Asserts that two XML are of identical content (namespace aliases are ignored).
     * 
     * @param expectedXml expected XML
     * @param actualXml actual XML
     * @throws Exception thrown if XML parsing fails
     */
    public static void assertEqualXmls(String expectedXml, String actualXml) throws Exception {
        // Find all namespace mappings
        Map<String, String> fullnamespace2newAlias = new HashMap<String, String>();
        generateNewAliasesForNamespacesFromXml(expectedXml, fullnamespace2newAlias);
        generateNewAliasesForNamespacesFromXml(actualXml, fullnamespace2newAlias);

        for (Entry<String, String> entry : fullnamespace2newAlias.entrySet()) {
            String newAlias = entry.getValue();
            String namespace = entry.getKey();
            Pattern nsReplacePattern = Pattern.compile("xmlns:(ns\\d+)=\"" + namespace + "\"");
            expectedXml = transletaNamespaceAliasesToNewAlias(expectedXml, newAlias, nsReplacePattern);
            actualXml = transletaNamespaceAliasesToNewAlias(actualXml, newAlias, nsReplacePattern);
        }

        // nomralize namespaces accoring to given mapping

        DocumentBuilder db = initDocumentParserFactory();

        Document expectedDocuemnt = db.parse(new ByteArrayInputStream(expectedXml.getBytes(Charset.forName("UTF-8"))));
        expectedDocuemnt.normalizeDocument();

        Document actualDocument = db.parse(new ByteArrayInputStream(actualXml.getBytes(Charset.forName("UTF-8"))));
        actualDocument.normalizeDocument();

        if (!expectedDocuemnt.isEqualNode(actualDocument)) {
            Assert.assertEquals(expectedXml, actualXml); //just to better visualize the diffeences i.e. in eclipse
        }
    }


    private static DocumentBuilder initDocumentParserFactory() throws ParserConfigurationException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(false);
        dbf.setCoalescing(true);
        dbf.setIgnoringElementContentWhitespace(true);
        dbf.setIgnoringComments(true);
        DocumentBuilder db = dbf.newDocumentBuilder();
        return db;
    }

    private static String transletaNamespaceAliasesToNewAlias(String xml, String newAlias, Pattern namespacePattern) {
        Matcher nsMatcherExp = namespacePattern.matcher(xml);
        if (nsMatcherExp.find()) {
            xml = xml.replaceAll(nsMatcherExp.group(1) + "[:]", newAlias + ":");
            xml = xml.replaceAll(nsMatcherExp.group(1) + "=", newAlias + "=");
        }
        return xml;
    }

    private static void generateNewAliasesForNamespacesFromXml(String xml, Map<String, String> fullnamespace2newAlias) {
        Matcher nsMatcher = NAMESPACE_PATTERN.matcher(xml);
        while (nsMatcher.find()) {
            if (!fullnamespace2newAlias.containsKey(nsMatcher.group(2))) {
                fullnamespace2newAlias.put(nsMatcher.group(2), "nsTr" + (fullnamespace2newAlias.size() + 1));
            }
        }
    }

}

它比较两个XML字符串,并通过将它们翻译为两个输入字符串中的唯一值来处理任何不匹配的命名空间映射。

可以进行微调,例如在翻译命名空间时。但对于我的要求,它只需要完成工作即可。


2

使用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();
    }
}

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

更多细节(包括assertThat(~).and(~)的配置和示例)请参见此文档页面

XMLUnit还具有(除其他功能外)DifferenceEvaluator以进行更精确的比较。

XMLUnit网站


1
这将比较完整的字符串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,因为客户端代码(测试代码)更加简洁。


1
我现在进行了两个测试,使用相同的XML和不同的XML,都能正常工作。通过IntelliJ diff工具,可以轻松地发现比较的XML之间的差异。 - Yngvar Kristiansen
1
顺便提一下,如果您使用Maven,需要这个依赖项:<dependency> <groupId>org.apache.santuario</groupId> <artifactId>xmlsec</artifactId> <version>2.0.6</version> </dependency> - Yngvar Kristiansen

0

如何在Java应用程序中使用JExamXML

    import com.a7soft.examxml.ExamXML;
    import com.a7soft.examxml.Options;

       .................

       // Reads two XML files into two strings
       String s1 = readFile("orders1.xml");
       String s2 = readFile("orders.xml");

       // Loads options saved in a property file
       Options.loadOptions("options");

       // Compares two Strings representing XML entities
       System.out.println( ExamXML.compareXMLString( s1, s2 ) );

-2

既然你说“语义等效”,我假设你想要做的不仅仅是字面上验证XML输出是否相等,而且你想要像这样的东西:

<foo> some stuff here</foo></code>

<foo>some stuff here</foo></code>

被视为等效。最终,重要的是你如何在从消息中重建对象时定义“语义等效”。只需从消息构建该对象并使用自定义equals()来定义你要查找的内容。


4
不是答案,而是一个问题。 - Kartoch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接