比较 RSpec 中 name/attribute 相等的 REXML 元素

Compare REXML elements for name/attribute equality in RSpec

在 RSpec 中是否有用于比较 REXML 元素逻辑相等性的匹配器?我尝试编写一个自定义匹配器,将它们转换为格式化字符串,但如果属性顺序不同,它会失败。 (因为noted in the XML spec,属性的顺序应该不重要。)

我可以通过编写一个比较名称、命名空间、子节点、属性等的自定义匹配器来磨合,但这似乎很耗时且容易出错,如果其他人已经这样做了我宁愿不重新发明轮子。

我最终使用 equivalent-xml gem and writing an RSpec custom matcher 将 REXML 转换为 Nokogiri,与 equivalent-xml 进行比较,并在需要时打印结果。

测试断言非常简单:

expect(actual).to be_xml(expected)

expect(actual).to be_xml(expected, path)

如果您想显示文件路径或某种标识符(例如,如果您要比较大量文档)。

匹配代码比它需要的更漂亮一些,因为它处理 REXML、Nokogiri 和字符串。

  module XMLMatchUtils
    def self.to_nokogiri(xml)
      return nil unless xml
      case xml
      when Nokogiri::XML::Element
        xml
      when Nokogiri::XML::Document
        xml.root
      when String
        to_nokogiri(Nokogiri::XML(xml, &:noblanks))
      when REXML::Element
        to_nokogiri(xml.to_s)
      else
        raise "be_xml() expected XML, got #{xml.class}"
      end
    end

    def self.to_pretty(nokogiri)
      return nil unless nokogiri
      out = StringIO.new
      save_options = Nokogiri::XML::Node::SaveOptions::FORMAT | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
      nokogiri.write_xml_to(out, encoding: 'UTF-8', indent: 2, save_with: save_options)
      out.string
    end

    def self.equivalent?(expected, actual, filename = nil)
      expected_xml = to_nokogiri(expected) || raise("expected value #{expected || 'nil'} does not appear to be XML#{" in #{filename}" if filename}")
      actual_xml = to_nokogiri(actual)

      EquivalentXml.equivalent?(expected_xml, actual_xml, element_order: false, normalize_whitespace: true)
    end

    def self.failure_message(expected, actual, filename = nil)
      expected_string = to_pretty(to_nokogiri(expected))
      actual_string = to_pretty(to_nokogiri(actual)) || actual

      # Uncomment this to dump expected/actual to file for manual diffing
      #
      # now = Time.now.to_i
      # FileUtils.mkdir('tmp') unless File.directory?('tmp')
      # File.open("tmp/#{now}-expected.xml", 'w') { |f| f.write(expected_string) }
      # File.open("tmp/#{now}-actual.xml", 'w') { |f| f.write(actual_string) }

      diff = Diffy::Diff.new(expected_string, actual_string).to_s(:text)

      "expected XML differs from actual#{" in #{filename}" if filename}:\n#{diff}"
    end

    def self.to_xml_string(actual)
      to_pretty(to_nokogiri(actual))
    end

    def self.failure_message_when_negated(actual, filename = nil)
      "expected not to get XML#{" in #{filename}" if filename}:\n\t#{to_xml_string(actual) || 'nil'}"
    end
  end

实际匹配器相当简单:

  RSpec::Matchers.define :be_xml do |expected, filename = nil|
    match do |actual|
      XMLMatchUtils.equivalent?(expected, actual, filename)
    end

    failure_message do |actual|
      XMLMatchUtils.failure_message(expected, actual, filename)
    end

    failure_message_when_negated do |actual|
      XMLMatchUtils.failure_message_when_negated(actual, filename)
    end
  end