The rspec-path_matchers gem
- Summary
- Installation
- Setup
- Usage
- Basic Assertions
- Attribute Assertions
- Directory Content & Nested Assertions
- Clear Failure Messages
- Available Options
- Added Value Over Standard RSpec Matchers
- Development
- Contributing
- License
Summary
RSpec::PathMatchers provides a comprehensive suite of RSpec matchers for testing directory structures.
Verifying that a generator, build script, or any file-manipulating process has produced the correct output can be tedious and verbose.
This gem makes it easy to express expectations on an entire directory tree and receive precise, easy-to-diagnose failure messages when that tree does not meet its expectations.
Installation
Add this line to your application's Gemfile
in the :test
or :development
group:
group :test, :development do
gem 'rspec-path_matchers'
end
OR add it to your project's gemspec
:
spec.add_development_dependency 'rspec-path_matchers'
And then execute:
bundle install
Setup
Require the gem in your spec/spec_helper.rb
file:
# spec/spec_helper.rb
require 'rspec/path_matchers'
RSpec.configure do |config|
config.include RSpec::PathMatchers
end
Usage
Basic Assertions
In their simplest forms, the be_dir
, be_file
, and be_symlink
matchers verify
the existence and type of the given path. Each also supports negative expectations.
# Check for existence and type
expect('/tmp/new_project').to be_dir
# Check for absence
expect('/tmp/new_project').not_to be_file
Attribute Assertions
All top-level and nested matchers can take options as a hash to assert on specific file attributes. The value of each option can be an RSpec matcher or a literal value.
expect('config.yml').to be_file(content: include('production'), size: be < 1000)
expect('app.sock').to be_symlink(target: '/var/run/app.sock')
Directory Content & Nested Assertions
The real power comes from describing the contents of a directory. The #containing
method asserts that a directory contains at least the specified entries, while
#containing_exactly
asserts that it has exactly those entries and no others.
These expectations can be nested to any depth to describe a complete directory tree.
expect('/var/www').to(
be_dir.containing(
file('index.html', mode: '0644'),
dir('assets').containing_exactly(
file('app.js'),
file('style.css')
),
no_file_named('config.php') # Assert that an entry does not exist
)
)
Methods available as arguments to the containing methods include dir
, file
,
symlink
, no_file_named
, no_dir_named
, and no_symlink_named
.
Clear Failure Messages
When an expectation is not met, this library gives well-formatted, easy-to-diagnose error messages. If the index.html file from the previous example had the wrong permissions AND style.css did not exist, the error would be:
'/var/www' was not as expected:
- index.html
expected mode to be '0644', but it was '0600'
- assets/style.css
expected it to exist
Available Options
Here is a list of all options that can be given to the matchers in this gem:
mode: <matcher|String>,
size: <matcher|Integer>, # only for file matchers
# Owner and group require a Unix-like platform that supports the Etc module.
owner: <matcher|String>,
group: <matcher|String>,
# See `File.birthtime`, `File.atime`, etc. for platform support
birthtime: <matcher|Time|DateTime>,
atime: <matcher|Time|DateTime>,
ctime: <matcher|Time|DateTime>,
mtime: <matcher|Time|DateTime>,
# Content matchers are only for file matchers
content: <matcher|String|Regexp>,
json_content: <matcher|true>,
yaml_content: <matcher|true>
# Target matchers are only for symlink matchers
target: <matcher|String>
target_type: <matcher|String|Symbol> # e.g., 'file', 'directory'
target_exist: <matcher|true|false>
Added Value Over Standard RSpec Matchers
Here’s a breakdown of the value this API provides over what is available in standard RSpec.
1. Abstraction and Readability: From Imperative to Declarative
Standard RSpec forces you to describe how to test something. This API allows you to declaratively state what the directory structure should look like.
Without this API (Imperative Style):
# This code describes the HOW: stat the file, get the mode, convert to octal...
# It's a script, not a specification.
path = '/var/www/html/index.html'
expect(File.exist?(path)).to be true
expect(File.stat(path).mode.to_s(8)[-4..]).to eq('0644')
expect(File.stat(path).owned?).to be true # This just checks if UID matches script runner
With this API (Declarative Style):
# This code describes the WHAT. The implementation details are hidden.
# It reads like a specification document.
expect('/var/www/html/index.html').to be_file(mode: '0644', owner: 'httpd')
This hides the complex, imperative logic inside the matcher and exposes a clean, readable, domain-specific language (DSL).
2. Conciseness and Cohesion: Grouping Related Assertions
Without this API, testing multiple attributes of a single file requires
fragmented, repetitive expect
calls. Your API groups these assertions into one
cohesive, logical block.
Without this API:
path = '/var/data/status.json'
expect(File.file?(path)).to be true
expect(File.size(path)).to be > 0
expect(File.read(path)).not_to include('error')
expect(JSON.parse(File.read(path))['status']).to eq('complete')
With this API:
expect('/var/data/status.json').to be_file(
size: be > 0,
content: not_to(match(/error/)),
json_content: include('status' => 'complete')
)
This is far easier to read and maintain because all the assertions about
status.json
are in one place.
3. The Nested Directory DSL Adds to the Expressive Power
This is where this API provides something that base RSpec simply cannot do elegantly. Describing the state of a directory tree with standard RSpec is incredibly verbose and difficult to read.
Without this API:
# This is hard to read and mentally parse.
dir_path = '/etc/service/nginx'
expect(Dir.exist?(dir_path)).to be true
expect(File.exist?(File.join(dir_path, 'run'))).to be true
expect(Dir.exist?(File.join(dir_path, 'log'))).to be true
expect(File.exist?(File.join(dir_path, 'down'))).to be false
With this API:
# This is a clear, hierarchical specification of the directory's contents.
expect('/etc/service/nginx').to(
be_dir.containing(
file('run'),
dir('log'),
no_file_named('down')
)
)
The nested containing
matchers allows you to write tests that humans
can make sense of.
4. Descriptive and Intelligible Failure Messages
When a complex, nested expectation fails, this gem pinpoints the exact failure, saving you valuable debugging time.
Standard RSpec Failure:
expected: true
got: false
This kind of message forces you to manually inspect the directory structure to understand what went wrong.
With this API:
You get a detailed, hierarchical report that shows the full expectation and clearly marks what failed.
For this expectation:
expect('config').to(
be_dir.containing(
file('database.xml', owner: 'db_user', mode: '0600')
)
)
'config' was not as expected:
- database.xml
expected owner to be "db_user", but it was "root"
expected mode to be "0600", but it was "0644"
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that
will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/main-branch/rspec-path_matchers. This project is intended to be a safe, welcoming space for collaboration.
License
The gem is available as open source under the terms of the MIT License.