Google-Fu: My RSpec script test is failing!
Is RSpec failing when you try to test a script with RSpec via the system
command? Here, for the benefit of any googlers who who can’t get their ruby
script working properly, is a possible fix to your problem!
tl;dr: jump to the bit you saw in the google search preview
The Setup
I recently created a small ruby project which is run via a small script, which
in turn, used some code stored in lib/
to perform a small function:
charter.rb
require_relative 'lib/charter'
Charter::Chart.show(ARG[0])
I can then run the script:
> ruby charter.rb ~/incoming/datafile.csv
I have a simple acceptance test to check that the script and Charter library code are working together correctly.
spec/charter_acceptance_spec.rb
# frozen_string_literal: true
module Charter
RSpec.describe 'command line invocation' do
it 'outputs a text chart' do
expected_output = "Happy: +++++\nHealthy: +++++\n"
expect { system 'ruby charter.rb testdata.csv' }
.to output(expected_output)
.to_stdout_from_any_process
end
end
end
This all works great, and I’m finding my script very useful. Nice.
Creating a Gem
Now… I then decided to turn this whole library into a gem, and while I’m at
it, put the script in the bin/
directory so it’s available as a command.
I create the gemspec1:
bin/Charter.gemspec
require_relative 'lib/charter/version'
Gem::Specification.new do |spec|
spec.name = 'Charter'
spec.version = Charter::VERSION
spec.authors = ['Mark Coleman']
spec.email = ['[email protected]']
spec.summary = %q{Charter implementation}
spec.description = %q{Output text chart for a given data set in csv format}
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
spec.files = ['bin/charter',
'lib/charter.rb',
'lib/charter/version.rb']
spec.bindir = 'bin'
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.require_paths = ['lib']
end
…which specifies the bin directory and executable file.
I also chmod
the script, rename it, and turn it into a ruby script:
bin/charter
#!/usr/bin/ruby
require_relative '../lib/charter'
Charter::Charter.output ARGV[0]
And I modify my spec
spec/charter_acceptance_spec.rb
...
expect { system 'bin/charter testdata.csv' }
.to output(expected_output)
.to_stdout_from_any_process
...
And that’s it. This is super easy, barely an inconvenience, so I run my tests again
The Problem
>bundle exec rspec
...
/Users/mark/.rbenv/versions/2.7.0/lib/ruby/2.7.0/bundler/spec_set.rb:86:in `block in materialize': Could not find byebug-11.1.3 in any of the sources (Bundler::GemNotFound)
...
Failure/Error:
expect { system 'bin/charter testdata.csv' }
.to output(expected_output)
.to_stdout_from_any_process
expected block to output "Happy: +++++\nHealthy: +++++\n" to stdout, but output nothing
...
What happened?
Maybe you’ve spotted the bug in the code above already, but let’s analyse the problem a little bit.
Investigation
Firstly, this code doesn’t use any gems. Gems are only used while I’m developing - rspec, byebug and some standard code metric stuff are in my default test group Gemfile.
Further, I can run rake install
and run this gem with charter datafile.csv
from the command line, and it works just fine. So, I know I don’t need the
bundler environment to run the code.
Rspec does run in the bundler environment, however.
On the other hand, the test uses system
to create a separate process to
execute bin/charter datafile.csv
. Maybe the environment for this process is
wrong. Why would that be? It worked for my original test 10 minutes ago when it
executed `ruby checker.rb testdata.csv’
So Many Questions
So, what is rspec doing when system
is run?
Does the process fork a seperate shell environment which copies the shell environment that is running rspec?
Is that what you actually want to happen for an acceptance test that is supposed to ensure a standalone gem command is working how you expect?
Why did it stop working from the non-gemified version?
Does bundling a gem do odd things when it sets up the bin
executable
directory and registers executables?
By default bundle gem
creates a gemfile that includes nearly everything in
your git repo when the gem is installed. Is that necessary?
Does RSpec or bundler handle projects with gemspecs slightly differently for some reason?
Is my rbenv setup inserting odd shims into my shell that is causing a problem?
The Solution
Let me know in the comments if you have answers to any of those questions but…
Nope, it’s none of those things.
The problem is here:
bin/charter
#!/usr/bin/ruby
require_relative '../lib/charter'
Charter::Charter.output ARGV[0]
The first line is called a
shebang. It’s the thing you add
to make a script file executable. So instead of running ruby script.rb
you
can run script
and it will be run with the specified interpreter.
I wrote:
#!/usr/bin/ruby
I should have written:
#!/usr/bin/env ruby
The former runs the system ruby; the latter runs ruby within the context of your shell environment (which in my case is controlled by rbenv, and will include the bundled gems that the rspec runner is expecting).
Changing that line makes everything work again.
The Conclusion
So, I think I’m done. However, I use rbenv to specify the version of ruby I’m using for each project I work on. It also manages gem sets for each one. If I change projects, since I’m not using the system version of ruby, I might need to re-install my new gem. This is a common issue I face with other gems I use that aren’t in the project’s Gemfile (like bropages).
I think the env
solution is OK, because if you use rbenv, you’ll be used to
this, and if you are just using the system ruby, the env
solution will
continue to point there anyway.
An Aside: A Rant about Magic Comments
Magic comments are bad design. Things that look like comments should have no effect on your code. Things that affect your code should not look like comments.
In this case the magic comment is a unix thing, but ruby has several examples,
like #frozen_string_literal: true
in the above snippets.
Particularly to a novice, it’s not clear that they are even any sort of syntax, and not just a comment (which is exactly what they look like).
# encoding: UTF-8
Is this a comment, or will it get intepreted by ruby?
Things that look like comments should have no effect. Things that affect your code should not look like comments.
Which of the following will be interpreted by ruby as a directive, and which as a comment?
# -*- encoding: big5 -*-
# encoding: UTF-8
# warn_indent: true
The answer is that all three lines will be interpreted as a directive.
What about the following? What do you expect the output to be?
# -*- encoding: big5 -*-
# encoding: UTF-8
# I like turtles
# frozen_string_literal: true
# warn_indent: true
# Simple test file to test ruby magic comments. NB: Could also have used
# warn_indent: false
def test_function
end
p ''.encoding
# warn_indent: true
The directive on the last line won’t be interpreted as such. The others will.
The second warn_indent
will take precedence, and no warning will be displayed.
On the other hand, the first encoding
directive will take precedence, and
encoding will be set to big5.
I hate magic comments. They are a lazy way to introduce backward compatibility.
-
As an aside, I don’t like the default gemspec template created by bundler:
# Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end
This puts the every non-testing file in your git repository into the gem. That’s a bit much. I might write a post about this some day. ↩