I highly suspect that the RSpec core team all use black backgrounds in their terminals because sometimes the colors aren’t so nice on my white terminal
I certainly use a black background. I'm not sure about the other RSpec
core folks. Regardless, if there are some color changes we can make that
would make output look good on a larger variety of backgrounds, we'll
certainly consider that (do you have some suggested changes?).
In the meantime, the colors are configurable, so you can change the colors
to fit your preferences on your machine. First, create a file at
~/.rspec. If this file exists, RSpec will use the command line options
configured in this file, allowing you to set personal preferences that
apply to any project on your machine. In this file put something like:
And then in
~/configure_rspec_colors.rb (or whatever file you decide
to require from
RSpec.configure do |c| # Each of these can be one of the ANSI color code integers or one # of [:black, :white, :red, :green, :yellow, :blue, :magenta, :cyan]. # The values I've assigned here are the defaults so you'll want to change them. # Color config is available in RSpec 2.13+. Since this file will # always be loaded by RSpec and you may have projects on earlier # versions, it's a good idea to only do this on versions that support it. if c.respond_to?(:default_color=) c.default_color = :white c.detail_color = :cyan c.failure_color = :red c.fixed_color = :blue c.pending_color = :yellow c.success_color = :green end end
But where is the
helpermethod defined? What’s it’s visibility? Can I put it in a module? Can I use inheritance? Who can call it? Can I call
superfrom the extracted method? If so, where does
supergo? These are not the types of questions I want to be pondering when I have 3000 test failures and really long spec files to read through. My mind spins off in to thoughts like “I wonder how RSpec works?”, and “I wonder what my cat is doing?”
From what I can gather, calling
describein RSpec essentially defines a class.
This is indeed what it does (plus some other stuff that I'll get into in a bit). I think it'll help you understand by showing an example:
describe "Using an array as a stack" do def build_stack  end def stack @stack ||= build_stack end it 'is initially empty' do expect(stack).to be_empty end context "after an item has been pushed" do def build_stack super.push :item end it 'allows the pushed item to be popped' do expect(stack.pop).to eq(:item) end end end
This example is almost exactly (sans some details such as RSpec assigning the classes to different constants) doing this:
class UsingAnArrayAsAStack < RSpec::Core::ExampleGroup set_it_up "Using an array as a stack" def build_stack  end def stack @stack ||= build_stack end it 'is initially empty' do expect(stack).to be_empty end class AfterAnIteHasBeenPushed < self set_it_up "after an item has been pushed" def build_stack super.push :item end it 'allows the pushed item to be popped' do expect(stack.pop).to eq(:item) end end children << AfterAnIteHasBeenPushed end RSpec.world.register UsingAnArrayAsAStack
As you can see, RSpec is in fact creating classes, just like you
guessed, and it uses
class_exec to evaluate the
Individual examples are evaluated in the context of an instance of
the class (like minitest!).
Nested contexts are simply subclasses, which means they inherit
all helper methods just like you would expect, and you can use
as you would expect. You can also see that RSpec does some extra stuff
set_it_up plus registering the classes via
children << and
RSpec.world.register for the top-level one) when you call
context. (I'd also point out that we do not consider
register to be part of our public API and this may
break in future versions...)
Can I put it in a module?
You can indeed. This works:
module MyHelpers def helper 11 end end describe "something" do include MyHelpers it "works" do expect(10).to eq(helper) end it "really works" do expect(11).to eq(helper) end end
If you wanted the module globally included in all example groups, you
include on config:
RSpec.configure do |config| config.include MyHelpers end
But if that’s the case, why not just use Ruby classes?
There are a few reasons for this:
- In its original formulation (as per Dave
and Dan North's articles introducing BDD),
one of the things BDD was about was using a new vocabulary to discuss testing, that, in their
experience, led you in a better direction when learning TDD.
itcome from that heritage. I think that they were on to something, but I also think that the alternate vocabulary is most useful for people learning TDD (as they discuss in their articles). For someone who has developed code using TDD for years, the alternate vocabulary probably doesn't matter much, and may even be a hindrance for someone very used to the xUnit style.
describeallows you to pass arbitrary English descriptions. Ruby classes have many restrictions on what is allowed in their names. Many people (including myself!) find it very useful to be able to use arbitrary English descriptions. I often include a rationale in my descriptions so that if a test ever fails, the developer is given an explanation for why the behavior expressed in the test was desirable at the time it was written (which can help them decide if they still need to maintain that behavior). For example, here's one test description from a project at work:
it 'ignores subsecond differences in timestamps since MySQL and redis store them at different granularities' do # ... end
- The fact that
describeis a method that accepts arguments means we can provide APIs for extensions to be able to get the "class being tested". This is a more explicit alternative to what
ActionController::TestCasedoes: rather than trying to infer the class-under-test from the test class name, the user gives it to us as an explicit argument. Explicit argument means less potential confusion when the user misspells it (they'll get an immediate NameError from ruby!), and the user doesn't have to know anything about how inferrence works.
describealso supports RSpec's metadata system, where example groups (test classes) and examples (individual test methods) have attached metadata that can be used to slice and dice your suite in many ways. For example, you can tag an example group as
describe MyClass, :slow do) and exclude groups with that tag (
rspec --tag "~slow"). rspec-rails uses
:typemetadata to know which Rails testing APIs to make available to which example groups. You can tag an example group as
:pendingto indicate that it should not yet pass, and RSpec will run it, expecting it to fail, and notify you if it now passes. These are all features that would be harder to support if you couldn't pass additional arguments when defining your test class.
You’ll see “hi!” printed 3 times, once for each it, and “hello!” printed once. For the nested context, I would like to print “hello!” before it prints “hi!”. In a normal inheritance situation, I would change the call to super. I am not sure how to do what I just described without learning a new thing
Honestly, I never thought about the fact that the
def setup style of
hooks allows you to control the order like this! This is indeed
something that doesn't have a simple solution in RSpec. I'll think more
about if there's anything we can do to improve it.
Overall, when I run into this type of situation -- where I've got a code
before hook, but an individual test needs to run something else
first -- I treat it as a code smell. It suggests that the code doesn't
really belong in a
before hook since it's not actually something that
we always want to run first. Usually I'll pull the code in the hook out
into a helper method and then explicitly call it from the places where it
needs to happen.
All that said, there is a way to accomplish what you are trying to do, but it's pretty obtuse:
describe "something" do def say_hi puts "hi!" end before do |ex| say_hi unless ex.metadata[:skip_hi] end # ... context "another thing", :skip_hi do before do puts "hello!" say_hi end # ... end end
This uses the metadata system (which I mentioned earlier) to skip the
logic in the before hook, and then in the nested group we can call it at
an appropriate time. As I said, that's far less elegant than using
super like you mentioned, and I don't recommend doing this, but it
could be a useful temporary step during a larger refactoring.
Another thing that bugs me is that if I do try to refactor my RSpec tests, and I change any lines, then the line that I copy and pasted to run just the one test won’t work anymore. I have to figure out the new line. I can use the name of the test by specifying the -e flag on RSpec, but then I lose the “copy, paste, run” feature that I love so much.
My suggestion is to use RSpec's focus-filtering feature. The
spec_helper.rb file generated by
rspec --init has a couple lines of
config that you'll need for this:
RSpec.configure do |config| config.filter_run :focus config.run_all_when_everything_filtered = true end
With that configured, you can (temporarily) add
:focus metadata to any
example or example group, and RSpec will run just those examples or
example groups. If nothing has
:focus, then everything will be run
(that's what the last line of config is for). With the
it doesn't matter if your specs move around to other lines. Given that
you'll only want to temporarily change the metadata, and how useful
this feature is, we also have some shortcuts:
fit is an alias for
fdescribe is an alias for
fcontext is an alias for
:focus -- so just prefix any
of these with the letter
f to temporarily make RSpec run only them.
Now, do I care which one you use? No. As long as you test your code, I am happy. A professional developer should be able to work in either one of these because they essentially do the same thing: test your code.
<3 <3 I completely agree with this. I'd go so far as to say that as lead RSpec maintainer, I'm extremely grateful that RSpec's not the only testing library around. It's good for the Ruby community that we have multiple well-maintained libraries for testing.
Thanks again, @tenderlove!