XML在配置文件、接口报文和数据交换中依然大量存在,但它的解析过程并不像看起来那么标准。同一份XML文档,换一个解析器、换一种读取方式,就可能出现完全不同的结果。测试阶段如果只关注业务字段,忽略编码、实体、命名空间和文本节点这些底层细节,往往会漏掉线上才会暴露的问题。下面结合几个真实测试场景,分析XML处理中容易踩到的坑。

一、编码与BOM字符导致解析器直接拒绝文档
很多测试环境生成的XML文件来自Windows记事本或某些导出工具,保存时默认携带UTF-8 BOM。文件开头三个不可见字节EF BB BF会被解析器当成XML声明之前的内容,于是Java DOM解析直接抛出“Content is not allowed in prolog”,而业务人员往往怀疑是报文格式写错了。这种情况在接口测试中尤其常见,因为手工编辑或工具转换都可能改变文件编码。
下面这段代码用最普通的读取方式加载XML,如果文件带有BOM,解析必然失败。
// 错误写法:BOM会被保留在字符串开头
String xml = new String(Files.readAllBytes(Paths.get("data.xml")), StandardCharsets.UTF_8);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml)));
修复思路并不复杂,读取字节后先判断首字符是否为BOM,再交给解析器。测试代码可以封装一个统一的XML读取工具,避免每个用例都重复处理。
// 修复:读取后判断并移除BOM
byte[] bytes = Files.readAllBytes(Paths.get("data.xml"));
String xml = new String(bytes, StandardCharsets.UTF_8);
if (!xml.isEmpty() && xml.charAt(0) == '\uFEFF') {
xml = xml.substring(1);
}
Document doc = builder.parse(new InputSource(new StringReader(xml)));
除了BOM,XML声明中的encoding属性与实际字节编码不一致也会导致乱码或解析失败。测试数据准备阶段应明确文件编码,必要时用十六进制查看文件头,增加BOM检测用例,避免只在开发环境偶然通过。
二、XML外部实体注入在测试环境中的复现与防护
XXE是XML安全测试中必须覆盖的一类问题。如果解析器使用默认配置,攻击者可以在XML头部声明外部实体,把本地文件内容拼进响应报文。测试环境经常发现因为未禁用DTD或外部实体,导致服务器返回 /etc/passwd 或 Windows 系统文件内容。这类漏洞不一定来自业务代码,更多是解析器工厂参数没配置好。
下面这段XML展示了外部实体注入的典型Payload。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE order [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <order> <customer>&xxe;</customer> </order>
用默认的Java解析器读取该文件,customer节点会直接输出服务器用户信息。测试时可以据此验证漏洞是否存在。
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("order.xml"));
System.out.println(doc.getElementsByTagName("customer").item(0).getTextContent());
防护方式是在解析前关闭DTD声明以及外部普通实体和参数实体。这些特性一旦关闭,包含DOCTYPE的文档会被拒绝,或者实体不会被展开。安全测试应增加断言,确保带有外部实体的XML不会触发文件读取。
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
DocumentBuilder builder = factory.newDocumentBuilder();
三、默认命名空间导致XPath查询不到节点
XML报文中很常见根节点声明了默认命名空间,例如根元素带 xmlns 属性。测试用例如果仍然使用 //book 或 /root/book 这样的XPath表达式,返回结果往往会空。原因在于XPath 1.0中未加前缀的节点名只匹配空命名空间,而带默认命名空间的元素属于另一个命名空间,两者无法对应。
下面这份XML声明了默认命名空间,元素本身看起来没有前缀,但实际命名空间已经不是空。
<?xml version="1.0" encoding="UTF-8"?>
<catalog xmlns="http://ipipp.com/ns">
<book id="1">
<title>XML Testing Guide</title>
</book>
</catalog>
直接用不带前缀的XPath查询 book 节点,结果是0条,容易让人误以为XML解析失败。
XPath xpath = XPathFactory.newInstance().newXPath(); String expr = "//book"; NodeList nodes = (NodeList) xpath.evaluate(expr, doc, XPathConstants.NODESET); System.out.println(nodes.getLength()); // 输出0
正确做法是为XPath设置NamespaceContext,将前缀映射到命名空间URI,并在表达式中使用前缀。这样可以准确匹配到默认命名空间里的节点。测试框架可以封装一层带命名空间上下文的方法,减少重复代码。
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
if ("ns".equals(prefix)) {
return "http://ipipp.com/ns";
}
return null;
}
@Override
public String getPrefix(String namespaceURI) { return null; }
@Override
public Iterator getPrefixes(String namespaceURI) { return null; }
});
String expr = "//ns:book";
NodeList nodes = (NodeList) xpath.evaluate(expr, doc, XPathConstants.NODESET);
四、特殊字符转义与CDATA带来的文本节点断言偏差
XML中 &、<、> 等字符有保留含义,文本内容如果包含这些字符,必须使用预定义实体或CDATA包裹,否则解析器会报错。测试数据来自用户输入或数据库时,未转义的 & 符号非常常见,会导致整个报文发送失败。另一个容易忽略的问题是CDATA在DOM解析后可能生成多个相邻文本节点,测试只读取第一个子节点会漏掉后半段内容。
下面的XML示例展示了非法写法和合法写法。非法写法中的 & 未转义,会直接导致解析错误。
<!-- 错误:文本中的 & 未转义 --> <item>AT&T</item> <!-- 正确:使用转义 --> <item>AT&T</item> <!-- 正确:使用CDATA包裹特殊字符 --> <item><![CDATA[AT&T <price>100</price>]]></item>
读取CDATA内容时,建议使用元素级别的 getTextContent() 方法,它会自动合并所有子文本节点。如果直接取第一个子节点的 nodeValue,可能只能拿到一部分内容,甚至因为空白节点返回空字符串。
NodeList items = doc.getElementsByTagName("item");
Element item = (Element) items.item(0);
String text = item.getTextContent();
System.out.println(text); // 获取完整文本,包含特殊字符
测试用例在设计断言时,应明确比较的是完整文本内容还是局部文本。对包含CDATA的字段,优先使用 getTextContent() 或遍历子节点合并文本,避免因节点拆分导致的用例偶发失败。
五、XML测试中建议固化的检查项
上述几个问题并非孤立存在,它们常常与解析器配置和测试数据准备方式相关。把XML测试拆成结构校验、安全校验和内容校验三层,可以让定位问题更清晰。结构层关注编码、BOM、标签闭合和命名空间;安全层关注DTD、外部实体和XInclude;内容层关注特殊字符、文本节点和XPath取值。
实际测试中可以把以下检查项固化到工具或框架里。
- 解析前检查BOM、编码声明与实际字节序是否一致;
- 安全测试中关闭DTD和外部实体,并验证关闭后异常处理;
- 对带命名空间的报文,XPath必须设置前缀映射;
- 对包含特殊字符的文本,统一使用合并文本节点的方式读取。
测试数据构造阶段可以保留一份包含非法字符、BOM、DOCTYPE、默认命名空间的样本库,每次接口联调或回归时直接复用。这样既能覆盖常见XML问题,也能避免临时拼报文时漏掉关键异常场景。