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.
4. Basic usage
4.1. Unified matcher
The be_serialization_equivalent_to matcher works with all formats:
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:
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
# 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.
# 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
# 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.
# 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
# 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
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
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
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.
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.
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
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:
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:
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:
# 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:
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:
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
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
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
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
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:
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]