1. Scope

This document describes how to use Canon’s RSpec matchers for testing. Canon provides semantic comparison matchers that focus on content rather than formatting.

For Ruby API usage, see Ruby API documentation.

For command-line usage, see CLI documentation.

2. General

Canon’s RSpec matchers use canonical (c14n) mode for comparison, ensuring that formatting differences don’t affect test results. Tests focus on semantic equivalence rather than exact string matching.

3. Installation

Add to your spec_helper.rb or spec/support/canon.rb:

require 'canon/rspec_matchers'

4. Basic usage

4.1. Unified matcher

The be_serialization_equivalent_to matcher works with all formats:

Example 1. Unified matcher syntax
require 'canon/rspec_matchers'

RSpec.describe 'Serialization' do
  it 'compares XML' do
    xml1 = '<root><a>1</a><b>2</b></root>'
    xml2 = '<root>  <b>2</b>  <a>1</a>  </root>'
    expect(xml1).to be_serialization_equivalent_to(xml2, format: :xml)
  end

  it 'compares HTML' do
    html1 = '<div><p>Hello</p></div>'
    html2 = '<div> <p> Hello </p> </div>'
    expect(html1).to be_serialization_equivalent_to(html2, format: :html)
  end

  it 'compares JSON' do
    json1 = '{"a":1,"b":2}'
    json2 = '{"b":2,"a":1}'
    expect(json1).to be_serialization_equivalent_to(json2, format: :json)
  end

  it 'compares YAML' do
    yaml1 = "a: 1\nb: 2"
    yaml2 = "b: 2\na: 1"
    expect(yaml1).to be_serialization_equivalent_to(yaml2, format: :yaml)
  end
end

4.2. Format-specific matchers

Canon provides dedicated matchers for each format:

Example 2. Format-specific matchers
RSpec.describe 'Format-specific matchers' do
  it 'uses XML matcher' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
    # or legacy alias
    expect(actual_xml).to be_analogous_with(expected_xml)
  end

  it 'uses HTML matcher' do
    expect(actual_html).to be_html_equivalent_to(expected_html)
  end

  it 'uses JSON matcher' do
    expect(actual_json).to be_json_equivalent_to(expected_json)
  end

  it 'uses YAML matcher' do
    expect(actual_yaml).to be_yaml_equivalent_to(expected_yaml)
  end
end

5. Global configuration

Configure matchers globally in your spec_helper.rb:

5.1. Syntax

Canon::RSpecMatchers.configure do |config|
  config.<format>.match.profile = :profile_name
  config.<format>.match.options = { ... }
  config.<format>.diff.mode = :mode_name
  config.<format>.diff.use_color = true/false
  config.<format>.diff.context_lines = n
  config.<format>.diff.grouping_lines = n
end

Where <format> is one of: xml, html, json, yaml

5.2. Match configuration

Example 3. Match profiles
# spec_helper.rb
Canon::RSpecMatchers.configure do |config|
  # Use spec_friendly profile for XML
  config.xml.match.profile = :spec_friendly

  # Use rendered profile for HTML
  config.html.match.profile = :rendered

  # Use strict profile for JSON
  config.json.match.profile = :strict
end

Available profiles: :strict, :rendered, :spec_friendly, :content_only

See Match options for profile details.

Example 4. Match dimension options
# spec_helper.rb
Canon::RSpecMatchers.configure do |config|
  config.xml.match.options = {
    text_content: :normalize,
    structural_whitespace: :ignore,
    attribute_order: :ignore,
    comments: :ignore
  }

  config.html.match.options = {
    text_content: :normalize,
    structural_whitespace: :ignore
  }

  config.json.match.options = {
    key_order: :ignore
  }
end

See Match options for dimension reference.

5.3. Diff configuration

Example 5. Diff mode and colors
# spec_helper.rb
Canon::RSpecMatchers.configure do |config|
  # XML with by-line mode
  config.xml.diff.mode = :by_line
  config.xml.diff.use_color = true

  # HTML with by-line mode
  config.html.diff.mode = :by_line

  # JSON with by-object mode (default)
  config.json.diff.mode = :by_object
  config.json.diff.use_color = true
end

See Diff modes for mode details.

Example 6. Diff formatting
# spec_helper.rb
Canon::RSpecMatchers.configure do |config|
  # Show 5 lines of context
  config.xml.diff.context_lines = 5

  # Group changes within 10 lines
  config.xml.diff.grouping_lines = 10

  # Disable colors for CI environments
  config.xml.diff.use_color = !ENV['CI']
end

See Diff formatting for options.

5.4. Complete configuration example

Example 7. Full configuration
# spec_helper.rb
require 'canon/rspec_matchers'

Canon::RSpecMatchers.configure do |config|
  # XML configuration
  config.xml.match.profile = :spec_friendly
  config.xml.match.options = {
    text_content: :normalize,
    structural_whitespace: :ignore,
    comments: :ignore
  }
  config.xml.diff.mode = :by_line
  config.xml.diff.use_color = true
  config.xml.diff.context_lines = 3

  # HTML configuration
  config.html.match.profile = :rendered
  config.html.diff.mode = :by_line
  config.html.diff.grouping_lines = 2

  # JSON configuration
  config.json.match.profile = :spec_friendly
  config.json.match.options = {
    key_order: :ignore
  }
  config.json.diff.mode = :by_object
  config.json.diff.context_lines = 5

  # YAML configuration
  config.yaml.match.options = {
    key_order: :ignore
  }
end

6. Per-test configuration

Override global configuration on a per-test basis using matcher options.

6.1. Using match profiles

Example 8. Override with profile
RSpec.describe 'Override global config' do
  it 'uses strict matching for this test' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
      .with_profile(:strict)
  end

  it 'uses rendered profile for HTML' do
    expect(actual_html).to be_html_equivalent_to(expected_html)
      .with_profile(:rendered)
  end
end

6.2. Using match options

Example 9. Override with specific dimensions
RSpec.describe 'Override specific dimensions' do
  it 'requires strict whitespace for this test' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
      .with_options(
        structural_whitespace: :strict,
        text_content: :strict
      )
  end

  it 'ignores attribute order' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
      .with_options(attribute_order: :ignore)
  end
end

6.3. Combining profile and options

Example 10. Profile with overrides
RSpec.describe 'Combine profile and options' do
  it 'uses spec_friendly but checks comments' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
      .with_profile(:spec_friendly)
      .with_options(comments: :strict)
  end

  it 'uses rendered but requires strict text' do
    expect(actual_html).to be_html_equivalent_to(expected_html)
      .with_profile(:rendered)
      .with_options(text_content: :strict)
  end
end

6.4. Verbose output

Use verbose: true to show detailed diff output on test failures.

Example 11. Verbose diff examples
RSpec.describe 'Verbose diff output' do
  it 'shows detailed XML diff on failure' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml, verbose: true)
  end

  it 'shows detailed HTML diff on failure' do
    expect(actual_html).to be_html_equivalent_to(expected_html, verbose: true)
  end

  it 'shows detailed JSON diff on failure' do
    expect(actual_json).to be_json_equivalent_to(expected_json, verbose: true)
  end

  # Combine with profile and options
  it 'shows verbose diff with custom options' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml, verbose: true)
      .with_profile(:spec_friendly)
      .with_options(comments: :strict)
  end
end

6.5. Display filtering

Use .show_diffs() to control which differences appear in failure output.

Example 12. Display filtering modes
RSpec.describe 'Display filtering' do
  it 'shows all differences (default)' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
      .show_diffs(:all)
  end

  it 'shows only normative differences' do
    # Only show differences that affect equivalence
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
      .show_diffs(:normative)
  end

  it 'shows only informative differences' do
    # Only show differences that don't affect equivalence
    expect(actual_xml).to be_xml_equivalent_to(expected_xml,
      match: { comments: :ignore }
    ).show_diffs(:informative)
  end
end
Example 13. Combining with match options
RSpec.describe 'Combined filtering' do
  it 'ignores comments but shows only normative diffs' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
      .with_options(comments: :ignore)
      .show_diffs(:normative)
  end

  it 'uses spec_friendly profile with normative filtering' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
      .with_profile(:spec_friendly)
      .show_diffs(:normative)
  end
end
Display filtering does NOT affect equivalence determination. It only controls which differences appear in the failure output. Equivalence is always based on normative differences only.

See Display filtering for complete details.

7. Diff algorithms

Canon supports two diff algorithms that can be selected based on your needs:

  • :dom (default): Positional, DOM-based matching

  • :semantic: Tree-based matching with operation detection

7.1. DOM algorithm

The DOM algorithm performs positional matching and is the default behavior:

Example 14. DOM algorithm usage
RSpec.describe 'DOM algorithm' do
  it 'uses positional matching by default' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
  end

  it 'explicitly specifies DOM algorithm' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml,
      diff_algorithm: :dom,
      verbose: true
    )
  end
end

7.2. Semantic algorithm

The semantic algorithm uses tree-based matching and detects operations like insert, delete, update, move, merge, and split:

Example 15. Semantic algorithm usage
RSpec.describe 'Semantic algorithm' do
  it 'detects semantic operations' do
    xml1 = '<root><a>1</a><b>2</b></root>'
    xml2 = '<root><b>2</b><a>1</a><c>3</c></root>'

    expect(xml1).to be_xml_equivalent_to(xml2,
      diff_algorithm: :semantic,
      verbose: true
    )
  end

  it 'identifies element moves' do
    html1 = '<div><p>First</p><p>Second</p></div>'
    html2 = '<div><p>Second</p><p>First</p></div>'

    expect(html1).to be_html_equivalent_to(html2,
      diff_algorithm: :semantic,
      verbose: true
    )
  end

  it 'detects insertions and deletions' do
    json1 = '{"users": [{"id": 1}, {"id": 2}]}'
    json2 = '{"users": [{"id": 1}, {"id": 2}, {"id": 3}]}'

    expect(json1).to be_json_equivalent_to(json2,
      diff_algorithm: :semantic,
      verbose: true
    )
  end
end

7.3. Global algorithm configuration

Configure the diff algorithm globally:

Example 16. Global diff algorithm configuration
# spec_helper.rb
Canon::RSpecMatchers.configure do |config|
  # Use semantic algorithm for XML by default
  config.xml.diff.algorithm = :semantic

  # Use DOM algorithm for HTML (default)
  config.html.diff.algorithm = :dom

  # Use semantic algorithm for JSON
  config.json.diff.algorithm = :semantic
end

7.4. Per-test algorithm override

Override the global algorithm configuration for specific tests:

Example 17. Per-test algorithm override
RSpec.describe 'Algorithm override' do
  # Global config uses :semantic
  # But this test uses :dom
  it 'uses DOM algorithm for this specific test' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml,
      diff_algorithm: :dom,
      verbose: true
    )
  end

  # Global config uses :dom
  # But this test uses :semantic
  it 'uses semantic algorithm to detect operations' do
    expect(actual_html).to be_html_equivalent_to(expected_html,
      diff_algorithm: :semantic,
      verbose: true
    )
  end
end

7.5. Combining algorithm with other options

The diff algorithm can be combined with match profiles and options:

Algorithm with profiles and options
RSpec.describe 'Combined configuration' do
  it 'uses semantic algorithm with spec_friendly profile' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml,
      diff_algorithm: :semantic,
      verbose: true
    )
      .with_profile(:spec_friendly)
  end

  it 'uses semantic algorithm with custom match options' do
    expect(actual_xml).to be_xml_equivalent_to(expected_xml,
      diff_algorithm: :semantic,
      verbose: true
    )
      .with_options(
        structural_whitespace: :ignore,
        comments: :ignore
      )
  end

  it 'uses DOM algorithm with strict matching' do
    expect(actual_html).to be_html_equivalent_to(expected_html,
      diff_algorithm: :dom,
      verbose: true
    )
      .with_profile(:strict)
  end
end

== Common patterns

=== Testing XML generation

XML generation tests
RSpec.describe 'XML generation' do
  let(:expected_xml) do
    <<~XML
      <document>
        <title>Test Document</title>
        <content>
          <p>Paragraph content</p>
        </content>
      </document>
    XML
  end

  it 'generates correct XML structure' do
    actual_xml = generate_xml(title: 'Test Document',
                               content: '<p>Paragraph content</p>')

    expect(actual_xml).to be_xml_equivalent_to(expected_xml)
  end

  it 'handles attributes correctly' do
    actual = generate_element(id: '123', class: 'active')
    expected = '<element class="active" id="123"/>'

    # Attribute order doesn't matter with default config
    expect(actual).to be_xml_equivalent_to(expected)
  end
end

7.6. Testing HTML output

HTML output tests
RSpec.describe 'HTML generation' do
  let(:expected_html) do
    <<~HTML
      <!DOCTYPE html>
      <html>
        <head>
          <title>Test Page</title>
        </head>
        <body>
          <h1>Welcome</h1>
          <p>Content here</p>
        </body>
      </html>
    HTML
  end

  it 'generates correct HTML' do
    actual_html = generate_page(
      title: 'Test Page',
      heading: 'Welcome',
      content: 'Content here'
    )

    expect(actual_html).to be_html_equivalent_to(expected_html)
  end

  # Formatting differences are ignored
  it 'ignores whitespace differences' do
    compact = '<div><p>Text</p></div>'
    pretty = <<~HTML
      <div>
        <p>Text</p>
      </div>
    HTML

    expect(compact).to be_html_equivalent_to(pretty)
  end
end

=== Testing JSON APIs

JSON API tests
RSpec.describe 'API responses' do
  let(:expected_response) do
    {
      "user" => {
        "id" => 123,
        "name" => "John Doe",
        "email" => "john@example.com"
      },
      "status" => "success"
    }.to_json
  end

  it 'returns correct user data' do
    response = api_client.get_user(123)

    expect(response).to be_json_equivalent_to(expected_response)
  end

  # Key order doesn't matter with default config
  it 'ignores key ordering' do
    actual = '{"b":2,"a":1}'
    expected = '{"a":1,"b":2}'

    expect(actual).to be_json_equivalent_to(expected)
  end
end

7.7. Testing configuration files

Configuration file tests
RSpec.describe 'Configuration files' do
  it 'generates correct YAML config' do
    config = generate_config(
      database: { host: 'localhost', port: 5432 },
      logging: { level: 'info' }
    )

    expected = <<~YAML
      database:
        host: localhost
        port: 5432
      logging:
        level: info
    YAML

    expect(config).to be_yaml_equivalent_to(expected)
  end

  it 'handles nested structures' do
    actual_yaml = to_yaml(deeply: { nested: { structure: 'value' } })
    expected_yaml = "deeply:\n  nested:\n    structure: value"

    expect(actual_yaml).to be_yaml_equivalent_to(expected_yaml)
  end
end

== Troubleshooting

=== Debugging test failures

When a test fails, Canon shows a detailed diff. Use verbose mode for maximum detail:

Debugging example
it 'shows exactly what differs' do
  expect(actual).to be_xml_equivalent_to(expected, verbose: true)
    .with_profile(:strict)  # Use strict to see all differences
end

The diff output will show:

  • Line numbers

  • Color-coded changes (red/green)

  • Whitespace visualization

  • Non-ASCII character warnings

=== Temporarily disabling global config

.Override global config
[example]

it 'uses different config for this test' do # Global config uses spec_friendly, but we want strict here expect(actual).to be_xml_equivalent_to(expected) .with_profile(:strict) .with_options( text_content: :strict, structural_whitespace: :strict ) end

=== Checking what's being compared

.Debug preprocessed content
[example]

# In your test, temporarily add: result = Canon::Comparison.equivalent?(actual, expected, verbose: true)

# Inspect preprocessed content puts "Preprocessed actual:" puts result[:preprocessed][0]

puts "Preprocessed expected:" puts result[:preprocessed][1]

puts "Differences:" pp result[:differences]

== See also

* link:../ruby-api/[Ruby API documentation]
* link:../cli/[Command-line interface]
* link:../../features/match-options/[Match options reference]
* link:../../understanding/diff-modes/[Diff modes]
* link:../../features/diff-formatting/[Diff formatting options]