Skip to content

Instantly share code, notes, and snippets.

@myronmarston
Last active April 6, 2022 19:47
Show Gist options
  • Star 43 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save myronmarston/9c21b85c784871161d36 to your computer and use it in GitHub Desktop.
Save myronmarston/9c21b85c784871161d36 to your computer and use it in GitHub Desktop.

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:

--require "~/configure_rspec_colors.rb"

And then in ~/configure_rspec_colors.rb (or whatever file you decide to require from ~/.rspec) put:

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 helper method defined? What’s it’s visibility? Can I put it in a module? Can I use inheritance? Who can call it? Can I call super from the extracted method? If so, where does super go? 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 describe in 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 describe block. 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 super 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 describe or context. (I'd also point out that we do not consider set_it_up, children and 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 can call 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 Astel's 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. describe and it come 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.
  • describe allows 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 describe is 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::TestCase does: 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.
  • describe also 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 :slow (describe MyClass, :slow do) and exclude groups with that tag (rspec --tag "~slow"). rspec-rails uses :type metadata to know which Rails testing APIs to make available to which example groups. You can tag an example group as :pending to 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 in a 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 :focus metadata 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 it with :focus, fdescribe is an alias for describe with :focus and fcontext is an alias for context with :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!

@nathanl
Copy link

nathanl commented Mar 6, 2015

@myronmarston Neat! Keep on cranking out the awesome! 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment