codegourmet

savory code and other culinary highlights

Comparing Deeply Nested Structures

| Comments

For testing a JSON API response I recently had to compare two deeply nested structures. To do this programmatically would be quite a pain, so I wrote a gem which tests a prepared structure (imported JSON) against the output of my (unit test) code.

General

Use case: you’re writing an API response or a JSON export and want to unit test it. Optionally you can ignore the leaf values or any hash key types/order (see below).

The gem gives error results with the path to where exactly the structures differ.

This post is a mirror of the gem’s README on github: https://github.com/codegourmet/structure_compare

Installation

1
gem install structure_compare

or add it to your Gemfile:

1
gem structure_compare

quick-n-dirty example:

1
2
3
4
5
6
7
8
9
require 'structure_compare'
comparison = StructureCompare::StructureComparison.new

expected = { a: 1, b: 2, c: [1, 2, 3] }
actual   = { a: 1, b: 2, c: [1, 2, "A"] }

comparison.structures_are_equal?(expected, actual)
puts comparison.error
# => root[:c][2] : expected String to be kind of Fixnum

MiniTest

1
2
3
4
5
require 'structure_compare'
require 'structure_compare/minitest'

assert_structures_equal({ a: 1, b: 2 }, { a: 1, b: 2 })
refute_structures_equal({ a: 1, b: 2 }, { c: 1, d: 2 })

Options

Strict key ordering

name: strict_key_order (default: false)

1
2
3
4
5
6
expected = { a: 1, b: 2 }
actual   = { b: 2, a: 1 }

comparison = StructureCompare::StructureComparison.new(strict_key_order: false)
comparison.structures_are_equal?(expected, actual)
# => true


Value checking

name: check_values (default: true)

1
2
3
4
5
6
7
8
9
10
expected = { a: 1, b: { c: 1 } }
actual   = { a: 8, b: { c: 8 } }

comparison = StructureCompare::StructureComparison.new
comparison.structures_are_equal?(expected, actual)
# => false

comparison = StructureCompare::StructureComparison.new(check_values: false)
comparison.structures_are_equal?(expected, actual)
# => true


Indifferent Access

Hash symbol keys are treated as equal to string keys

NOTE: an exception will be raised if there’s a key present as symbol and string

name: indifferent_access (default: false)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
expected = { a: 1 }
actual   = { "a" => 1 }

comparison = StructureCompare::StructureComparison.new
comparison.structures_are_equal?(expected, actual)
# => false

comparison = StructureCompare::StructureComparison.new(indifferent_access: true)
comparison.structures_are_equal?(expected, actual)
# => true

hash = { a: 1, "a" => 2 }
comparison = StructureCompare::StructureComparison.new(indifferent_access: true)
comparison.structures_are_equal?(hash, hash)
# => StructureCompare::IndifferentAccessError


Float tolerance

When dealing with floats, you will want to introduce a tolerance.
NOTE: Float::EPSILON is always used for comparing Float type values.
NOTE: The check_values option must be set.

name: float_tolerance_factor (default: 0)

1
tolerance = +- (expected * (1.0 + tolerance_factor) + Float::EPSILON)

A float_tolerance_factor setting of 0.01 means that actual can be 1% different from expected to still be treated equal.

1
2
3
4
5
6
7
8
9
10
11
12
13
expected = { a: 10.0 }
actual_1 = { a: 10.1 }
actual_2 = { a: 10.11 }

# 1% tolerance factor
comparison = StructureCompare::StructureComparison.new(
  float_tolerance_factor: 0.01, check_values: true
)
comparison.structures_are_equal?(expected, actual_1)
# => true

comparison.structures_are_equal?(expected, actual_2)
# => false


TODOS

RSpec helpers.

Refactoring.

Contributing

Fork me and send me a pull request with your feature and working tests, or just request a feature.

License

MIT License, see LICENSE file in the root directory

Happy Coding! – codegourmet

Comments