data_spec

Build Status Dependency Status Code Climate

Easily compare hashes and arrays in RSpec and Cucumber

Originally inspired by collectiveidea's json_spec gem

Installation

gem 'data_spec'

Cucumber

Setup:

Include data_spec and define data in your Cucumber environment:

# features/support/env.rb
require "data_spec"

def data
  #...
end

Usage:

The step Given the data is: is not supplied by this gem

Use either YAML or JSON:

Given the data is:
"""
chunky: bacon
"""
Then the data should be:
"""
{
  "chunky": "bacon"
}
"""
```

Use path selection:
```ruby
Given the data is:
"""
interleaved:
- hashes:
    and:
    - arrays
"""
Then the data at "interleaved/0/hashes" should be:
"""
and:
- arrays
"""
```

Check inclusion:
```ruby
Given the data is:
"""
- 1
- 2
- 3
- even:
    'in a': hash
    'with only': some keys
"""
Then the data includes:
"""
- 1
- 2
"""
And the data at "3/even" includes "'in a': hash"
```

Check types:
```ruby
Given the data is:
"""
- bacon
- 1
- 2013-07-06 20:09:32.824102000 -07:00
- "https://www.google.com/images/srpr/logo4w.png"
"""
Then the data at "0" should be of type String
Then the data at "1" should be of type Fixnum
Then the data at "2" should be of type Time
Then the data at "3" should be of type URI
```
Note: Checking a URI involves actually fetching the URI

Use embedded code:
```ruby
Given the data is:
"""
- `1+1`
- '$1e2$'
- `'bacon'.class`
"""
Then the data at "0" should be 2
Then the data at "1" should be 100
Then the data at "2" should be `"chunky".class`
```
(Among other things, this lets you work around Ruby YAML's lack of support for scientific notation)

**Note: This is done via a raw `eval`, so it's dangerous**

Store data for later use:
```ruby
Given `@samples` is:
"""
where: "http://google.com"
when: `Time.now`
"""
And `@samples` includes:
"""
what: {}
"""
And `@samples['what']` is:
"""
- chunky
- bacon
"""
And the data is:
"""
meal: 
  main_course: `@samples['what'][1]`
  style: `@samples['what'][0]`
  ordered: `@samples['when']`
"""
Then the data should be:
"""
meal:
  main_course: bacon
  style: chunky
  ordered: `@samples['when']`
"""
```

Again, this is done by raw eval, so it's dangerous, and it's definitely enough to shoot your foot off with.

If you're using this in anything like a complex sense, look up the "evaluation" and "remember" helpers, below


Steps
----------

Matching:
* `Then the data should be:`
* `Then the data should be "..."`
* `Then the data at "..." should be:`
* `Then the data at "..." should be "..."`

Inclusion:
* `Then the data includes:`
* `Then the data includes "..."`
* `Then the data at "..." includes:`
* `Then the data at "..." includes "..."`

Type checking:
* `Then the data is of type ...`
* `Then the data at "..." if of type ...`

Pathing is done like so: `data[:chunky]['Bacon'][0]` would be "chunky/bacon/0". Each element (when looking in a hash) is first tried as a symbol, then as a string.

When checking inclusion against an array, you need to supply an array: `[1,2,3]` includes `[2]`, or `[1, [2,3], 4]` includes `[[2,3]]`.

When checking inclusion against a hash, you need to supply a hash: `{one: :two, three: four}` includes `{one: :two}`

You don't see this quite the same in the Cucumber steps because YAML parsing from a string does this inherently: `"one: two"` becomes `{'one' => 'two'}` and
`"[one, two]"` or `"- one\n- two"` becomes `['one', 'two']`

# RSpec

Setup
--------
```ruby
#in spec/spec_helper.rb
require 'data_spec'
```

Matchers
--------
Three matchers:
* `match_data(...).at(...)`
* `includes_data(...).at(...)`
* `match_block(lambda{...}).at(...)`

Exact matching is handled by `==`, while partial matching is handled by 
[`deep_include?`](http://stackoverflow.com/questions/3826969/ruby-hash-include-another-hash-deep-check)

Note that pathing is applied to the object being checked:

    hash1.should match_data(hash2).at("path/0")

results in:

    hash1[:path][0].should match_data(hash2)

# Library

Helpers
--------
* `DataSpec::Helpers.at_path(data, path)`
* `DataSpec.parse`

`at_path` is what provides the "pathing" functionality, while `parse` provides the interpreting of embedded code.

As seen in the examples, you can use $ at the beginning and end, or backticks instead of quotes. 
The backticks will actually be converted to dollar signs (YAML parsers choke on backticks), 
but they're prettier and easier to read.

* `DataSpec::Helpers.evaluate(string)`

This simply does an `eval` on the string; however, _because it also used when parsing YAML/JSON_, you can add variables
in which it is run, and use those variables in your YAML and JSON. 

* `DataSpec::Helpers.remember(string, data)

This is another way to add data to the evaulation scope; pass in the complete variable name (such as `@sample_data`) and the data to be stored.

Refinements
-------
Defined in `DataSpec::Refinements`; to use, `using DataSpec::Refinements`

* `Array.tree_walk_with_self{|(key, value), array| ... }`
* `Array.deep_include? sub_array`
* `Hash.tree_walk_with_self{|(key, value), hash| ... }`
* `Hash.deep_include? sub_hash`

`tree_walk_with_self` allows you to apply a block to every key/value pair in the hash or array, traversing recursively.
The third yielded value is the current node: `hash[key] == value`  - this allows you to alter the values of the hash during traversal.

`Array.deep_include? sub_array` simply does `(sub_array - self).empty?`, which is true when all elements of the sub-array
are present in `self`

`Hash.deep_include? sub_hash` is used to detect if every key/value pair in the `sub_hash` is present in `self`

#Contributing

Remaining Issues
-------
* The error messages suck. Plan is to provide them as a diff'd YAML, although I'm not sure what to do for blocks
* No table syntax as in `json_spec`
* No explicit testing of `tree_walks_with_self`
* No support for XML

Pull Requests
-----------
Go for it! Accepted code will have Cucumber and RSpec testing and be minimalist; if you spot a bug, try to provide a failing test in the report.

"Minimalist" doesn't mean fewest lines of code (although that's usually the case); it means "fewest new functions and objects"