diff --git a/spec/custom_matchers/equal_namespace_definitions_spec.rb b/spec/custom_matchers/equal_namespace_definitions_spec.rb
new file mode 100644
index 0000000..02b51aa
--- /dev/null
+++ b/spec/custom_matchers/equal_namespace_definitions_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+RSpec.describe 'equal_namespace_definitions matcher' do
+ let(:xml) { Examples::Example2.xml }
+ let(:xml2) { Examples::Example2.xml }
+
+ it 'successfully compares two similar xml strings' do
+ expect(xml).to have_equal_namespace_definitions_as xml2
+ end
+
+ context 'when one of the classes is not a string' do
+ let(:string) { 'hello' }
+ let(:integer) { 1234 }
+
+ it 'fails' do
+ expect(string).not_to have_equal_namespace_definitions_as integer
+ end
+
+ it 'provides a useful error message' do
+ expect { expect(string).to have_equal_namespace_definitions_as integer }
+ .to raise_error(RSpec::Expectations::ExpectationNotMetError, /does not have equal namespaces as/)
+ end
+ end
+
+ context 'when one of the strings does not contain valid xml' do
+ let(:xml2) { 'foobar' }
+
+ it 'fails' do
+ expect(xml).not_to have_equal_namespace_definitions_as xml2
+ end
+
+ it 'provides a useful error message' do
+ expect { expect(xml).to have_equal_namespace_definitions_as xml2 }
+ .to raise_error(RSpec::Expectations::ExpectationNotMetError, /does not have equal namespaces as/)
+ end
+ end
+
+ context 'when one xml string is different' do
+ let(:xml2) { Examples::Example3.json }
+
+ it 'fails the comparison' do
+ expect(xml).not_to have_equal_namespace_definitions_as xml2
+ end
+
+ it 'provides a useful error message' do
+ expect { expect(xml).to have_equal_namespace_definitions_as xml2 }
+ .to raise_error(RSpec::Expectations::ExpectationNotMetError, /does not have equal namespaces as/)
+ end
+ end
+
+ context 'with namespaces' do
+ let(:xml) do
+ <<-XML
+
+ david
+ frank
+
+ XML
+ end
+
+ context 'with unequal namespace definitions' do
+ let(:xml2) do
+ <<~XML
+
+ david
+
+ frank
+
+
+ XML
+ end
+
+ it 'fails the comparison' do
+ expect(xml).not_to have_equal_namespace_definitions_as xml2
+ end
+
+ it 'provides a useful error message' do
+ expect { expect(xml).to have_equal_namespace_definitions_as xml2 }
+ .to raise_error(RSpec::Expectations::ExpectationNotMetError, /does not have equal namespaces as/)
+ end
+ end
+
+ context 'with similar namespace definitions' do
+ let(:xml2) do
+ <<~XML
+
+ david
+ frank
+
+ XML
+ end
+
+ it 'successfully compares two similar xml strings' do
+ expect(xml).to have_equal_namespace_definitions_as xml2
+ end
+ end
+ end
+end
diff --git a/spec/dachsfisch/bidirectional_converter_spec.rb b/spec/dachsfisch/bidirectional_converter_spec.rb
new file mode 100644
index 0000000..f32c789
--- /dev/null
+++ b/spec/dachsfisch/bidirectional_converter_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+RSpec.describe 'BidirectionalConverter' do
+ let(:xml2_json) { Dachsfisch::XML2JSONConverter }
+ let(:json2_xml) { Dachsfisch::JSON2XMLConverter }
+
+ context 'with valid XML' do
+ describe '#perform' do
+ subject { json2_xml.perform json: }
+
+ let(:json) { xml2_json.perform xml: }
+
+ Examples.each :json2xml do |example|
+ context "with #{example.name}" do
+ let(:xml) { example.xml }
+
+ it { is_expected.to be_equivalent_to(xml) }
+ it { is_expected.to have_equal_namespace_definitions_as(xml) }
+ end
+ end
+ end
+ end
+
+ context 'with valid JSON' do
+ describe '#perform' do
+ subject { xml2_json.perform xml: }
+
+ let(:xml) { json2_xml.perform json: }
+
+ Examples.each :xml2json do |example|
+ context "with #{example.name}" do
+ let(:json) { example.json }
+
+ it { is_expected.to be_an_equal_json_as(json) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/expectations/equal_namespace_definitions.rb b/spec/support/expectations/equal_namespace_definitions.rb
new file mode 100644
index 0000000..8f33794
--- /dev/null
+++ b/spec/support/expectations/equal_namespace_definitions.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'rspec/expectations'
+
+RSpec::Matchers.define :have_equal_namespace_definitions_as do |expected|
+ attr_reader :actual, :expected
+
+ match do |actual|
+ return false unless actual.is_a?(String) && expected.is_a?(String)
+
+ expected_xml = parse_fragment(expected)
+ @expected = expected_xml.to_xml
+ actual_xml = parse_fragment(actual)
+ @actual = actual_xml.to_xml
+
+ return false if expected_xml.errors.length.positive? || actual_xml.errors.length.positive?
+ return false unless EquivalentXml.equivalent?(expected_xml, actual_xml)
+
+ return compare_namespaces(actual_xml.children.first, expected_xml.children.first)
+ end
+
+ failure_message do |actual|
+ "#{@actual || actual} does not have equal namespaces as \n#{@expected || expected}."
+ end
+
+ diffable
+
+ private
+
+ def compare_namespaces(actual, expected)
+ return false unless same_namespace_definitions?(actual, expected)
+
+ actual.children.each_with_index.all? do |actual_child, index|
+ expected_child = expected.children[index]
+ compare_namespaces(actual_child, expected_child)
+ end
+ end
+
+ def same_namespace_definitions?(actual, expected)
+ return true if actual.nil? && expected.nil?
+ return false if actual.nil? || expected.nil?
+
+ actual_namespaces = namespaces(actual)
+ expected_namespaces = namespaces(expected)
+ actual_namespaces == expected_namespaces
+ end
+
+ def namespaces(node)
+ node.namespace_definitions.map {|namespace| namespace.deconstruct_keys(%i[prefix href]) }.sort_by {|ns| ns[:prefix].to_s }
+ end
+
+ def parse_fragment(xml)
+ # This is a workaround for an unintended behavior in Nokogiri's XML::DocumentFragment.parse method.
+ # Originally, the method de-duplicates the namespace definitions of all nodes in the fragment, if possible.
+ # However, for this test, we want to compare the namespace definitions of the actual and expected XML.
+ # Therefore, we parse the XML with a root node, and compare the resulting document.
+ Nokogiri::XML::Document.parse("#{xml}", &:noblanks)
+ end
+end