David Chelimsky

random thoughtlessness

rspec-rails-2.8.1 is released

| Comments

Bug fix release

The rails-3.2.0.rc2 release broke stub_model in rspec-rails-2.0.0 > 2.8.0. The rspec-rails-2.8.1 release fixes this issue, but it means that when you upgrade to rails-3.2.0.rc2 or greater, you’ll have to upgrade to rspec-rails-2.8.1 or greater.

Because rspec-rails-2.8.1 supports all versions of rails since 3.0, I recommend that you upgrade to rspec-rails-2.8.1 first, and then upgrade to rails-3.2.0.rc2 (or 3.2.0 once it’s out).

Changelog

http://rubydoc.info/gems/rspec-rails/file/Changelog.md

Docs

http://rubydoc.info/gems/rspec-rails

http://relishapp.com/rspec/rspec-rails

RSpec-2.8 is released!

| Comments

We released RSpec-2.8.0 today with a host of new features and improvements since 2.7. Some of the highlights are described below, but you can see the full changelogs at:

Documentation

While not 100% complete yet, we’ve made great strides on RSpec’s RDoc:

http://rspec.info is now just a one pager (desperate for some design love – volunteers please email rspec-users@rubyforge.org). All the old pages are redirects to the relevant RDoc at http://rubydoc.info. RSpec-1 info is still available at http://old.rspec.info.

We’ve still got Cucumber features up at http://relishapp.com/rspec, but we’re going to be phasing that out as the primary source of documentation. There are a lot of reasons for this, and I’ll try to follow up with a separate blog post on this topic.

rspec-core

Improved support for tags and filtering

You can now set default tags/filters in either RSpec.configure or a .rspec file and override these tags on the command line. For example, this configuration tells rspec to run all the examples that are not tagged :slow:

# in spec/spec_helper.rb
RSpec.configure do |c|
  c.treat_symbols_as_metadata_keys_with_true_values = true
  c.filter_run_excluding :slow
end

Now when you want run those, you can just do this:

rspec --tag slow

This will override the configuration and run onlly the examples tagged :slow.

—order rand

We added an --order option with two supported values: rand and default.

rspec --order random (or rand) tells RSpec to run the groups in a random order, and then run the examples within each group in random order. We implemented it this way (rather than complete randomization of every example) because we don’t want to re-run expensive before(:all) hooks. A fair tradeoff, as the resulting randomization is just as effective at exposing order-dependency bugs.

When you use --order random, RSpec prints out the random number it used to seed the randomizer. When you think you’ve found an order-dependency bug, you can pass the seed along and the order will remain consistent:

--order rand:3455

--order default tells RSpec to load groups and examples as they are declared in each file.

rspec —init

We added an --init switch to the rspec command to generate a “spec” directory, and “.rspec” and “spec/spec_helper.rb” files with some starter code in them.

rspec-expectations

We discovered that the matcher DSL generates matchers that run considerably slower than classes which implement the matcher protocol. We made some minor improvements in the DSL, but to really improve things we re-implemented every single built-in matcher as a class.

rspec-2.8.0.rc1 is released

| Comments

I just released rspec-2.8.0.rc1, which includes releases of rspec-core, rspec-expectations, rspec-mocks, and rspec-rails. Changelogs for each are at:

What’s new

Nothing really changed in rspec-rails or rspec-mocks, but rspec-core and rspec-expectations have both gotten some nice improvements.

Configuration (rspec-core)

rspec-core offers a number of configuration options which can be declared on the command line, in a config file (.rspec, ~/.rspec, or custom location), as well as in an RSpec.configure block (in spec/spec_helper.rb by convention). Before this release, some options, but not all, could be stored in config files and then overridden on the command line. The problems were that it was inconsistent (not all options worked this way), and we couldn’t override options that were set in RSpec.configure blocks.

With this release, almost all options declared in RSpec.configure can be overridden from the command line, and --tag options can override their inverses. For example, if you have this in .rspec:

--tag ~slow:true

That means “exclude examples tagged :slow => true”. So the following example would be excluded:

it "does something", :slow => true do
  # ...
end

You can also exclude that example from RSpec.configure like this:

RSpec.configure do |c|
  c.filter_run_excluding :slow => true
end

Note: the naming is different for historical reasons, and we will reconcile that in a future release, but for now, just know that --tag on the command line and in .rspec is synonymous with filter_run_[including|excluding] in RSpec.configure.

Override from command line

Whether the default is stored in .rspec or RSpec.configure, it can be overridden from the command line like this:

rspec --tag slow:true

“Profiles” in custom options files

The rspec command has an --options option that let’s store command line args in arbitrary files and tell RSpec where to find them. For example, you could set things up so your normal spec run excludes the groups and examples marked :slow by putting this in .rspec:

--tag ~slow

Now add a .slow file with:

--tag slow

Now run rspec to run everything but the slow specs, and run rspec --options .slow or rspec -O.slow to run the slow ones.

Override from Rake task

RSpec’s Rake task supports an rspec_opts config option, which means you can set up different groupings from rake tasks as well. The fast/slow example above would look like this:

namespace :spec do
  desc "runs the fast specs"
  RSpec::Core::RakeTask.new(:fast) do |t|
    t.rspec_opts = '--options .fast'
  end
  RSpec::Core::RakeTask.new(:slow) do |t|
    t.rspec_opts = '--options .slow'
  end
end

Or ..

namespace :spec do
  desc "runs the fast specs"
  RSpec::Core::RakeTask.new(:fast) do |t|
    t.rspec_opts = '--tag ~slow'
  end
  RSpec::Core::RakeTask.new(:slow) do |t|
    t.rspec_opts = '--tag slow'
  end
end

Implicit true value for tags/filters

This is not new in rspec-2.8, but all the tags/filters in the example above can be written without explicitly typing true:

--tag slow
--tag ~slow







RSpec.configure {|c| c.filter_run_excluding :slow}

it "does something", :slow do

You have to set a config option to enable this in rspec-2.x:

RSpec.configure {|c| c.treat_symbols_as_metadata_keys_with_true_values = true}

In rspec-3.0, this will be the default, but without setting this value in 2.x you’ll get a deprecation warning when you try to configure things this way. It’s ugly, I know, but this enabled us to introduce the new behavior without breaking compatibility with some suites in a minor release.

Ordering

With 2.8, you can now run the examples in random order, using the new --order option:

--order rand

The order is randomized with some reasonable caveats:

  • top level example groups are randomized
  • nested groups are randomized within their parent group
  • examples are randomized within their group

This provides a very useful level of randomization while maintaining the integrity of before/after hooks, subject, let, etc.

If you want to run the examples in the default ordering (file-system load order for files and declaration order for groups/examples), you can override the order from the command line:

--order default

Pseudo-randomization

The randomization is managed by Ruby’s pseudo-randomization. This means that if you find an order dependency and want to debug/fix it, you can fix the order by providing the same seed for each run:

--order rand:1234

The seed is printed to the console with each run, so you can just copy it to the command. You can also just specify the seed, which RSpec will assume means you want to run with --order rand:

--seed 1234

Every time you run the suite with the same seed, the examples will run in the same “random” order.

Built-in matchers are all classes in rspec-expectations

The Matcher DSL in rspec-expectations makes it dead simple to define custom matchers that suit your domain. The problem is that it is several times slower than defining a class to do so. While this doesn’t make much difference when you have a custom matcher that you use a few dozen times (where talking hundredths of seconds here), it does make a difference if every single matcher invocation in your entire suite suffers this problem.

The short term fix is that all of the built-in matchers have been re-implemented as classes rather than using the DSL to declare them. This has the added benefit of making it easier to navigate the code and RDoc

Longer term, we’ll try to refactor the internals of the matcher DSL so that it generates a class at declaration time. Eventually.

Summing up

So that’s it. Nothing ground breaking. Nothing compatibility breaking. But some nice new features and improvements that will make your life just a little bit nicer when you upgrade. We’re doing a release candidate because enough changed internally that I want to give you time to try it out, so please, please do so! And please report any issues you’re having with this upgrade to:

Assuming there are no significant issues, I’ll release 2.8 final within a week or two.

Happy spec’ing!

David

rspec-2.7.0 is released!

| Comments

We’re pleased to announce the release of rspec-2.7.0. Release notes for each gem are listed below, but here are a couple of highlights:

Just type rspec

With the the 2.7.0 release, if you keep all of your specs in the conventional spec directory, you don’t need to follow the rspec command with a path. Just type rspec.

If you keep your specs in a different directory, just set the --default_path option to that directory on the command line, or in a .rspec config file.

The rake task now lets Bundler manage Bundler

The RSpec::Core::RakeTask invokes the rspec command in a subshell. In recent releases, it assumed that you wanted it prefixed with bundle exec if it saw a Gemfile. We then added gemfile and skip_bundler options to the task, so you could manage this in different ways.

It turns out that Bundler manages this quite well without any help from RSpec. If you activate Bundler in the parent shell, via the command line or Bundler.setup, it sets environment variables that activate Bundler in the subshell with the correct gemfile.

The gemfile and skip_bundler options are therefore deprecated and have no effect.

Release Notes

rspec-core-2.7.0

full changelog

NOTE: RSpec’s release policy dictates that there should not be any backward incompatible changes in minor releases, but we’re making an exception to release a change to how RSpec interacts with other command line tools.

As of 2.7.0, you must explicity require "rspec/autorun" unless you use the rspec command (which already does this for you).

  • Enhancements

    • Add example.exception (David Chelimsky)
    • --default_path command line option (Justin Ko)
    • support multiple --line_number options (David J. Hamilton)
      • also supports path/to/file.rb:5:9 (runs examples on lines 5 and 9)
    • Allow classes/modules to be used as shared example group identifiers (Arthur Gunn)
    • Friendly error message when shared context cannot be found (Sławosz Sławiński)
    • Clear formatters when resetting config (John Bintz)
    • Add xspecify and xexample as temp-pending methods (David Chelimsky)
    • Add --no-drb option (Iain Hecker)
    • Provide more accurate run time by registering start time before code is loaded (David Chelimsky)
    • Rake task default pattern finds specs in symlinked dirs (Kelly Felkins)
    • Rake task no longer does anything to invoke bundler since Bundler already handles it for us. Thanks to Andre Arko for the tip.
    • Add --failure-exit-code option (Chris Griego)
  • Bug fixes

    • Include Rake::DSL to remove deprecation warnings in Rake > 0.8.7 (Pivotal Casebook)
    • Only eval let block once even if it returns nil (Adam Meehan)
    • Fix --pattern option (wasn’t being recognized) (David Chelimsky)
    • Only implicitly require "rspec/autorun" with the rspec command (David Chelimsky)
    • Ensure that rspec’s at_exit defines the exit code (Daniel Doubrovkine)
    • Show the correct snippet in the HTML and TextMate formatters (Brian Faherty)

rspec-expectations-2.7.0

full changelog

  • Enhancements

    • HaveMatcher converts argument using to_i (Alex Bepple & Pat Maddox)
    • Improved failure message for the have_xxx matcher (Myron Marston)
    • HaveMatcher supports count (Matthew Bellantoni)
    • Change matcher dups Enumerable before the action, supporting custom Enumerable types like CollectionProxy in Rails (David Chelimsky)
  • Bug fixes

    • Fix typo in have(n).xyz documentation (Jean Boussier)
    • fix safe_sort for ruby 1.9.2 (Kernel now defines <=> for Object) (Peter van Hardenberg)

rspec-mocks-2.7.0

full changelog

  • Enhancements

    • Use __send__ rather than send (alextk)
    • Add support for any_instance.stub_chain (Sidu Ponnappa)
    • Add support for any_instance argument matching based on with (Sidu Ponnappa and Andy Lindeman)
  • Changes

    • Check for failure_message_for_should or failure_message instead of description to detect a matcher (Tibor Claassen)
  • Bug fixes

    • pass a hash to any_instance.stub. (Justin Ko)
    • allow to_ary to be called without raising NoMethodError (Mikhail Dieterle)
    • any_instance properly restores private methods (Sidu Ponnappa)

rspec-rails-2.7.0

full changelog

  • Enhancments

    • ActiveRecord::Relation can use the =~ matcher (Andy Lindeman)
    • Make generated controller spec more consistent with regard to ids (Brent J. Nordquist)
    • Less restrictive autotest mapping between spec and implementation files (José Valim)
    • require 'rspec/autorun' from generated spec_helper.rb (David Chelimsky)
    • add bypass_rescue (Lenny Marks)
    • route_to accepts query string (Marc Weil)
  • Internal

    • Added specs for generators using ammeter (Alex Rothenberg)
  • Bug fixes

    • Fix configuration/integration bug with rails 3.0 (fixed in 3.1) in which fixure_file_upload reads from ActiveSupport::TestCase.fixture_path and misses RSpec’s configuration (David Chelimsky)
    • Support nested resource in view spec generator (David Chelimsky)
    • Define primary_key on class generated by mock_model("WithAString") (David Chelimsky)

Avoid stubbing methods invoked by a framework

| Comments

In a github issue reported to the rspec-mocks project, the user had run into a problem in a Rails’ controller spec in which an RSpec-generated test double didn’t behave as expected. What follows is an edited version of the issue and my response, with the hope that it reaches a wider audience and/or sparks some conversation.

The reported problem: ActiveSupport::JSON::Encoding::CircularReferenceError using doubles

This spec …

require 'spec_helper'

describe ListsController do
  let(:list) { double("List") }

  describe "GET 'index'" do
    let(:expected) { [{id: "1", name: "test"}] }

    before do
      list.stub(:id){ "1" }
      list.stub(:name){ "test" }
      List.stub(:select){ [ list ] }
    end

    it "should return the list of lists" do
      get :index, format: :json
      response.body.should == expected.to_json
    end
  end
end

… plus this implementation …

class ListsController < ApplicationController
  respond_to :json

  expose(:lists) { List.select("id, name") }

  def index
    respond_with(lists)
  end
end

… produces this failure:

  Failure/Error: get :index, format: :json
     ActiveSupport::JSON::Encoding::CircularReferenceError:
       object references itself

The deeper problem: this is a great example of when not to use stubs.

Here’s why: there are three incorrect assumptions hiding behind the stubs!

  1. select takes an Array: List.select(["id","name"]), but the example stubs it incorrectly.
  2. the id is numeric, but the example uses String.
  3. the json is wrapped: {"list":{"id":1,"name":"test"}}, but the example doesn’t wrap it.

Even if the stubs were properly aligned with reality, the reason for the error is that respond_with(lists) eventually calls as_json on the list object, which, in this example, is an RSpec double that doesn’t implement as_json. We need to either use a stub_model (which does implement as_json), or explicitly stub it in the example:

list.stub(:as_json) { { list: {id: 1, name: "test"} } }

But I’d avoid stubs altogether in this case. Stubs are great for well defined (and understood) public APIs which are invoked by the code being specified. In this case, we’re stubbing an API (as_json) that is invoked by the Rails framework, not the code being specified. If the Rails framework ever changes how it renders json, this example would continue to pass, but it would be a false positive.

One possible remedy

Here’s how I’d approach this outside-in (based on my own flow, design preferences, and target outcomes. YMMV.)

Start with a request spec:

require 'spec_helper'

describe "Lists" do
  describe "GET 'index.json'" do
    it "returns the list of lists" do
      list = List.create!(name: "test")
      get "/lists.json"
      response.body.should == [{list: {id: list.id, name: "test"}}].to_json
    end
  end
end

This shows exactly what to expect, so when working on clients we can refer directly to this without having to dig into internals.

Run this and it fails with uninitialized constant List, so generate the list resource:

rails generate resource list name:string
rake db:migrate
rake db:test:prepare

Run it again and it fails with ActionView::MissingTemplate. Now we have a couple of choices. The purist view says “write a controller spec”, but some people say controller specs are unnecessary if there are already request specs (or cukes) as they just add duplication.

For me, the answer depends upon the complexity of the requirement as it compares to what we get for free from Rails. In this case, the only difference between the requirement and what Rails gives us for free is that we constrain the fields to id and name This is something we can implement in the model, so I’d just implement this very simple controller code and move on:

class ListsController < ApplicationController
  respond_to :json

  def index
    respond_with List.all
  end
end

Now the request spec fails with:

expected: "[{\"list\":{\"id\":1,\"name\":\"test\"}}]"
     got: "[{\"list\":{\"created_at\":\"2011-08-27T14:56:19Z\",\"id\":1,\"name\":\"test\",\"updated_at\":\"2011-08-27T14:56:19Z\"}}]"

We’re getting more key/value pairs than we want. I want the model responsible for constraining the keys in the json (Rails implements json transformations in the context of the model, so why shouldn’t we?), so I’d add a model spec:

require 'spec_helper'

describe List do
  describe "#as_json" do
    it "constrains keys to id and name" do
      list = List.new(:name => "things")
      list.as_json['list'].keys.should eq(%w[id name])
    end
  end
end

This fails with:

expected ["id", "name"]
     got ["created_at", "name", "updated_at"]

I expect to see created_at and updated_at, but I’m surprised (initially) to see that id is missing. Thinking this through, it makes sense because the example generates the list using new, so no id is generated. To get id to show up in the list of keys, we can use create instead of new, or we can explicitly set it. I’m going to go with setting the id explicitly to avoid the db hit, accepting the self-imposed leaky abstraction. It’s all trade-offs.

it "constrains fields to id and name" do
  list = List.new(:name => "things")
  list.id = 37
  list.as_json['list'].keys.should eq(%w[id name])
end

Now it fails with:

expected ["id", "name"]
     got ["created_at", "id", "name", "updated_at"]

Now we can implement the constraint:

class List < ActiveRecord::Base
  def as_json
    super({ only: %w[id name]})
  end
end

Now the model spec passes, but the request spec fails with:

ArgumentError:
  wrong number of arguments (1 for 0)

This is because the as_json implementation fails to honor the Rails API:

as_json(options = nil)

as_json is called by the Rails framework with an options hash. Had we done this without the request spec and weren’t aware of this information, we’d have a bunch of passing specs but the app would blow up. Hooray for testing at multiple levels!

So we add a new example to the model spec:

it "honors the submitted options hash" do
  list = List.new(:name => "things")
  list.id = 37
  list.as_json(:only => :name)['list'].keys.should eq(%w[name])
end

This fails with wrong number of arguments (1 for 0) as well, so now we adjust the model implementation:

def as_json(opts={})
  super({ only: %w[id name]}.merge(opts))
end

Now the model spec passes again, and so does the request spec! DONE!

The result is a very nice balance of clarity, speed (in spite of the one db hit in the request spec) and flexibility. Any new endpoints we add will get the same json representation because it is expressed in the model (heeding the principle of least surprise). The model spec not only specifies how the model should represent itself as json, but it helps to explain how the rails framework uses the model. All of this with no stubbing at all, and especially no stubbing of APIs our code isn’t invoking.

Stop typing “bundle exec”

| Comments

Bundler serves two primary purposes:

  1. it helps you to install the correct gem versions
  2. it constrains the load path to the correct gem versions at runtime

Assuming you’re using Bundler to constrain your runtime environment (which you are if you’re using Rails 3 defaults), then you are likely prefixing most shell commands with bundle exec.

We interrupt this post for an important update:

Two important pieces of information in the comments:

  1. Prepending ./bin to your path exposes a serious security risk. Proceed with caution.
  2. rvm >= 1.6.18 + bundler >= 1.0.5 removes the need for this altogether.

We now return you to our regularly scheduled post:

Here’s a little tip to help save you the prefix, without adding any aliases or functions to your environment.

bundle install --binstubs
export PATH=./bin:$PATH

bundle install --binstubs creates a bin directory at the root of your project, and fills it with Bundler-enabled wrappers for all of the executables installed by the gems listed in your Gemfile. This enables you to type bin/rake instead of bundle exec rake, for example, ensuring that the correct version of rake is loaded.

Now prepend ./bin to your path and you can just type rake.

rspec-core-2.6.4 is released!

| Comments

full changelog

  • Bug fixes
    • Support exclusion filters in DRb. (Yann Lugrin)
    • Fix —example escaping when run over DRb. (Elliot Winkler)
    • Use standard ANSI codes for color formatting so colors work in a wider set of color schemes.

rake 0.9 and gem version constraints

| Comments

There’s been some confusion surrounding the rake-0.9.0 release, and I’d like to take the opportunity to clarify some things and hopefully draw attention to gem versioning policies and their implications for everybody in the Ruby community.

First, there are three distinct issues related to the rake release:

1. Backward-incompatibility

Rake 0.9 includes backward-incompatible changes. Per the changelog:

## Version 0.9.0

* *Incompatible* *change*: Rake DSL commands ('task', 'file', etc.)  are
  no longer private methods in Object.  If you need to call 'task :xzy' inside
  your class, include Rake::DSL into the class.  The DSL is still available at
  the top level scope (via the top level object which extends Rake::DSL).

This conflicts with the way Rails, among others, uses Rake, among others. The workaround recommended by @dhh is to constrain the rake version in the Gemfiles in your Rails applications:

gem "rake", "0.8.7"

This is a perfectly fine short term solution to keep your applications running, but it won’t be long before a gem that your Rails application depends on, either directly or through the transitive property of dependencies, is going to specify any of:

gemspec.add_dependency 'rake', '0.9.0'
gemspec.add_dependency 'rake', '>= 0.9.0'
gemspec.add_dependency 'rake', '~> 0.9.0'

When that happens, you’ll need to loosen the constraint in your app if you want to upgrade any of the gems downstream from the gem that introduces this dependency. This is not a big deal because you can control the situation directly in your own Gemfile in your own application.

Libraries are not applications

This advice should not, however, be applied to any gems that depend on Rake. Let’s say you’re using two gems that both provide Rake tasks and therefore depend on the rake gem. At some point the maintainer of gem aaa changes the constraint to "= 0.8.7", and the maintainer of gem bbb keeps a looser constraint: either ">= 0.8.7" or "~> 0.8.7". You upgrade to the new version of aaa and everything is fine because both constraints are satisfied by rake-0.8.7.

A little while down the road, the constraint in bbb changes to "~> 0.9.0". At this point you are unable to have the newest versions of aaa and bbb in the same application. This may not seem like a big deal because you can choose to not upgrade bbb at this point, but the further upstream the dependency (i.e. aaa depends on bbb, which depends on ccc), the more likely you are to be constrained in your upgrade choices.

In short, if you are maintaining a gem that applications or other gems depend on, you are doing end users a disservice by locking down any upstream dependency at one and only one version number.

Now here’s the catch: while some gem maintainers follow some sort of standard versioning and/or release policy, there are many that don’t. If you put in a looser version constraint on a gem whose maintainers introduce breaking changes in patch releases, you are also doing your users a disservice. More on this later.

2. Rake is used to run tasks that depend on Rake

Perhaps you’ve run into this interaction (or similar):

$ bundle install
$ rake db:migrate
You have already activated rake 0.9.0, but your Gemfile requires rake 0.8.7. Consider using bundle exec.

In this case, the application has an explicit dependency on rake-0.8.7, but rake-0.9.0 is installed in the shell environment. When you type rake xxx, Rubygems activates the 0.9.0 version (the newest version installed), and then tries to activate 0.8.7 when the app is running.

This is a catch 22 that we’ve been lulled into ignoring by the mere fact that there have not been any rake releases for a couple of years (rake-0.9.0 was released 2 years and 5 days after rake-0.8.7). We all expect to type rake xxx and have it just work. Why not? It’s worked thus far, right?

During the two years of rake-0.8.7, Bundler was born. You may remember that the Bundler team took a lot of heat during its early days. One of the complaints I remember was that people didn’t want to have to type bundle exec to run a rake task. The result is that pretty much all apps that use Bundler and Rake have this in their Rakefiles:

require 'rubygems'
require 'bundler'
Bundler.setup

This enables us to type rake xxx and let Bundler manage loading every other gem besides rake, which is already loaded by Rubygems. So now when we find both rake-0.8.7 and 0.9.0 in our gem environment, and the app we’re working with depends explicitly on 0.8.7, we have (at least) three options:

a. Tell bundler to install the rake command PROJECT_ROOT/bin

bundle install --binstubs

Now you can run this

bin/rake xxx

b. Explicitly run bundle exec

bundle exec rake

In either of the first two options, Bundler controls the activation of the rake gem for you, which allows it to put the correct version on the $LOAD_PATH.

c. Just remove 0.9 from the current gem environment

gem uninstall rake

This only works if you’re using an isolated gemset for the current project (e.g. using rvm) or you simply don’t need rake-0.9.0 on your system. It also is not a very reliable way to deal with this if you have any sort of automated build or deployment system that is installing gems into a shared gem environment on the build or production servers.

The real problem here is not that we have to type a different command on the command line. We humans can adapt and get used to doing that. The deeper problem is that there are countless automation scripts out in the wild that depend on rake xxx. In order to support both versions of Rake, they will all have to be changed to use one of the first two solutions noted above. The cost of this is no small chunk of change, but it is nobody’s fault but our own for failing to recognize the cyclical nature of using a versioned tool to run applications that might require a different version.

3. Not all gems expose their dependencies in a way that Bundler or Rubygems can control them

On my team at DRW, we tried to constrain our rake dependencies to 0.8.7 as a temporary measure, but each time we installed into a new gem environment we found that rake-0.9.0 was being installed. It turned out that a gem we depended on was installing rake through a back door, and with no version constraint at all. The result was that neither Bundler nor Rubygems had any control over this installation relative to our application (Bundler told Rubygems to install this gem, and this gem silently installed rake). And, to make things more confusing, Bundler reported that it was installing rake-0.8.7 and said nothing about 0.9.0.

The maintainer of that gem released new versions right away, so that issue is now resolved, but it’s entirely possible that other gems you’re using are doing the same (or similar). Just something to keep your eye out for.

What can we learn from all of this?

One issue this exposes is a lack of common understanding and agreement about how to manage releases and dependencies. The Rubygems Rational Versioning policy and Semantic Versioning are both very sound approaches that share a common scheme for version numbers:

A version has three parts: major, minor, and patch. For example, release 3.0.0 is a major release because the first number was incremented from 2 to 3, 3.2.0 is a minor release because the second number was incremented from 1 to 2, and 3.2.1 is a patch release because the third number was incremented from 0 to 1. Both specs state the following:

  1. Patch releases (3.2.1) should only include bug fixes and internal implementation changes.
  2. Minor releases (3.2.0) can include bug fixes, internal changes, and new features, but no breaking changes.
  3. Major releases (3.0.0) can include bug fixes, internal changes, new features, and breaking changes.

If everybody adhered to either policy, we’d all be able to declare our gem dependencies like this:

spec.add_dependency "foo", ">= 2.3", "< 3.0"

… or the following, oft misunderstood, shortcut for same:

spec.add_dependency "foo", "~> 2.3"

This tells Rubygems to install the newest version that is >= 2.3.0, trusting that no version 2.y.x will include breaking changes.

RSpec

I’ll confess that I didn’t adhere to either approach with RSpec until the rspec-2.0.0 release, last October. I knowingly introduced breaking changes in the 1.x series and RSpec likely lost the confidence of a fair sum of users during that time.

The good news, vis a vis RSpec, is that we’ve been following Rubygems Rational Versioning since the rspec-2.0 release. While we’ve had a couple of regressions in the process (followed swiftly by patch releases that addressed them), there has been only one intentionally breaking change, and that was related to integration with another library. That change was announced, documented, and I don’t recall seeing any issues reported related to it.

We’re not doing SemVer yet because it is more strict than RRV, and RSpec does not currently meet all of its criteria. I do hope, however, to have RSpec on SemVer before the year is out.

This all sounds great, but …

… the reality is that getting every gem developer to commit to RRV or SemVer is very unlikely. What those of us who do can do, however, is try to provide a balance of flexibility and safety when we declare upstream dependencies. The rspec-expectations gem, for example, declares the following runtime dependency:

diff-lcs ~> 1.1.2

This expresses an opinion that it is safe for your application (that depends on rspec-expectations) to depend on any 1.1.x version of diff-lcs greater than or equal to 1.1.2, but it is not safe to depend on 1.2.0. While this provides a high degree of safety, it also provides low flexibility: if any other gem your app depends on depends on diff-lcs-1.2 in the future (not likely, since 1.1.2 was released in 2004, but that’s besides the point), you won’t be able to use it with the current release of rspec-expectations, even if the diff-lcs-2.2 release does not include any breaking changes.

If diff-lcs was still under regular maintenance, and it’s maintainers were committed to RRV or SemVer, then rspec-expectations would be able to use this dependency instead:

diff-lcs ~> 1.2

This would provide significantly more flexibility in rspec-expectations’s ability to play nice with other gems that also depend on diff-lcs in the same applciation over a longer period of time.

Note that every gem page on rubygems.org now includes a recommendation to use the pessamistic constraint using a three-part version number (e.g. rake ~> 0.9.0). As just discussed, this provides safety, but lacks long term flexibility.

Depending on rake

So what should maintainers of gems that depend on rake do now? The likelihood is that some end users will constrain their applications to rake 0.8.7, and others will constrain them to = 0.9.0, ~> 0.9.0, or >= 0.9.0. Unless Jim Weirich announces that rake will follow RRV or SemVer, we have to allow for the possibility that rake 0.10.0 will introduce new breaking changes. In this case, I think the responsible thing to do is make sure our gems work with both rake-0.8 and 0.9, and specify the dependency like this:

spec.add_runtime_dependency 'rake', '>= 0.8.7', '< 0.10'

Trusting that no rake 0.9.x version will introduce breaking changes, this provides the greatest flexibility to end users without exposing them to the risk of breaking changes in rake-0.10.0.

Feedback

I’m curious to hear what you think about all of this. Do you think this all makes sense? Do you think I’m over or understating the importance, complexity, or severity of these issues? Do you have a different approach to recommend in moving forward? I look forward to your feedback.

rspec-rails-2.6.1 is released!

| Comments

This is a bug fix release that is compatible with the rails-3.0.0 to 3.0.7, 3.0.8.rc1, and 3.1.0.rc1 (it is mostly, but not fully compatible with but not rails-3.1.0.beta1).

rspec-rails-2.6.1 / 2011-05-25

full changelog

  • Bug fixes
    • fix controller specs with anonymous controllers with around filters
    • exclude spec directory from rcov metrics (Rodrigo Navarro)
    • guard against calling prerequisites on nil default rake task (Jack Dempsey)