In praise of Shoulda macros

Shoulda contexts let you to share setup code between different tests. This is for me one of Shoulda’s most attractive features.

When you combine this with the technique of defining your own macros to encapsulate assertions or setups that come up often, you end up with seriously DRY and readable tests.

I see a few different kinds of Shoulda macros:

Assertion macros

Assertions macros often begin with should_. They encapsulate one or a few assertions.

For ActiveRecord models:

should_require_attributes :name, :phone_number

They may even accept a block and do assertions on its execution, like should_raise:

should_raise(LoadError, :message => /vespene/) do
  require "more vespene gas"
end

To learn more about assertion macros, you can take a look at Shoulda macros allows you to embrace your inner slacker by Josh Nichols. Inner slacker? I’m right there!

Setup macros

This kind of macro encapsulates a setup that comes up often in your test suite. One inspired by Restful Authentication’s login_as helper method could be used like this:

logged_in_as :mat do
  # Shoulda tests
end

These kinds of macros accept a block that defines more Shoulda tests, rather than a block of code testing your app per se.

Turnkey macros

Turnkey macros are beefed up assertion macros. The main difference is their extent. They contain many contexts and a lot of should blocks. They usually accept substantial options hashes or are configured with a setup block. Like should_be_restful in the following example, inspired by the Shoulda documentation:

logged_in_as :stranger do
  should_be_restful do |resource|
    resource.create.params   = { :subject => "test", :body => "message" }
    resource.denied.actions  = [:edit, :update, :destroy]
    resource.denied.redirect = "login_url"
    resource.denied.flash    = /only the owner can/i
  end
end

Two Shoulda best practices around setup macros

This article is specifically about setup macros.

Here’s the implementation of a pretty generic Shoulda macro I could define in my test_helper*. This is an implementation of the macro I mentioned at the beginning:

# Sets the current person in the session from the person fixtures.
def self.logged_in_as(person, &block)
  context "logged in as #{person}" do
    setup do
      @request.session[:person] = people(person).id
    end

    yield
  end
end

Which can then be used like this in any controller test:

logged_in_as :mat do
  # tests for users
end

logged_in_as :admin do
  # tests for admin
end

Setup macros have a very subtle catch, however. Here’s a modified version of the first example above:

logged_in_as :mat do
  setup do
    @request.session[:last_login] = Time.now
  end
  # Some tests
end

The setup block you see here is never going to be executed. Why?

If we were to replace the logged_in_as macro by the actual code it contains, here’s what it would look like:

context "logged in as #{person}" do
  setup do
    @request.session[:person] = people(person).id
  end
  setup do
    @request.session[:last_login] = Time.now
  end
  # Some tests
end

Does that make sense? Not so sure.

Shoulda doesn’t like to have multiple setup blocks for a given context. That part does make sense.

Best practice #1: Always describe the situation with a context.

You should always describe the situation in which your test takes place (what your setup is doing) with a context.

logged_in_as :mat do
  context "with last login set to now" do
    setup do
      @request.session[:last_login] = Time.now
    end
    # Some tests
  end
end

Fair enough. We blame it on the user of the macro :-)

Since we’re using Ruby, most of us are probably in agreement with Matz’ “Make the programmer happy” motto.

So can we also solve the problem from the other end? Create a setup macro that supports a direct inner setup block? Of course we can, this is Ruby, not VB.

Best practice #2: Create setup macros that support a second setup block

A setup is grafted to a context that describes it. As the creator of the macro, I don’t know what crazy setup blocks programmers will put inside their macro. So I simply create a mute context:

# Sets the current person in the session from the person fixtures.
def self.logged_in_as(person, &block)
  context "logged in as #{person}" do
    setup do
      @request.session[:person] = people(person).id
    end

    context '' do
      yield
    end
  end
end

Now my macro supports the following test without a hitch:

logged_in_as :mat do
  setup do
    @request.session[:last_login] = Time.now
  end
  # Some tests
end

And of course, programmers who stick to best practice #1 can still write a cleaner test without a problem. The awesomeness of contexts lies in the fact that they can be nested:

logged_in_as :mat do
  context "with last login set to now" do
    setup do
      @request.session[:last_login] = Time.now
    end
    # Some tests
  end
end

Conclusion

Best practice #1 is simple. A setup block should be described by its encompassing context. It’s a question of readability. Nesting a setup block immediately inside a Shoulda macro is a dubious practice.

Best practice #2 is a more pragmatic solution to the problem. Ok, nesting a block right inside a macro isn’t always the best idea.

But when you don’t have the macro right under your nose, it may take you a while before you think about looking at said macro. I don’t know about you, but I have a tendency to have a great deal of confidence in macros that work well across my test suite.

So after you’ve spent an hour questioning Shoulda (or your sanity, or whether you should have become a gardener instead of a software developer) because your setup block isn’t executing, best practice #2 starts to make sense.

It may or may not be necessary in all your setup macros. I find it’s especially useful for macros that are generic enough to be used across your test suite. Or most of all, in setup macros you will share with the world.

Best practice #2 makes setup macros bulletproof to the problem of multiple setups.

Now go refactor your setup macros!

To learn more about Shoulda, check out Thoughtbot’s comprehensive documentation.

* A note on where to define Shoulda macros. Shoulda 2 can now auto load macros that are in the right location. This will help you keep your test_helper cleaner. Read more about it succinctly in Shoulda can automatically load custom macros by Josh Nichols or in the Shoulda 2.0 release post.

If you use Shoulda and, like me, you hate Test::Unit’s assert_raise(), I may have something of interest for you.

Why the hate?

Well, assert_raise accepts an *args list of exception types.

If you don’t pass any, you get some nonsense because an empty array doesn’t jive with the exception raised by your block. Useful. So if you don’t care what exception is raised, assert_raise isn’t gonna help you.

Also, assert_raise doesn’t let you specify what kind of exception message you’re expecting. I actually don’t mind that a given assertion should verify exactly one thing. On the other hand I have to jump through hoops to capture the exception if I want to assert on the error message.

A shoulda macro to the rescue

As usual, Shoulda is there to help us keep our test code DRY and intuitive. I’ve concocted a useful macro called should_raise. Here’s the gist:

It must be called with the block you expect to raise an exception, of course. You can also specify two optional arguments, the exception type and the message.

:kind_of or :instance_of

If not specified, an assertion is made that an exception was raised, but with no restriction on the type of the exception.

If you specify :kind_of, the assertion will be that much more precise. It will check that the exception raised was of the type specified, or a descendant.

If you specify :instance_of, the assertion is now that the exception raised was exactly of the type specified.

A shorthand is also available, where the type of the exception is supplied directly, like should_raise(LoadError), in which case the assertion is the same as with :instance_of.

In all of these cases, exactly one assertion is generated, whether or not :type is specified.

:message

If :message is specified, a second assertion will be added in order to make sure the error message matches the parameter. This can either be a string or a regex. The assertion is simply an assert_match.

If not specified, no assertion is generated for the message.

Examples

should_raise do
  require "more vespene gas"
end
# 1 assertion

should_raise(LoadError) do
  require "more vespene gas"
end
# 1 more restrictive assertion

should_raise(:instance_of => LoadError) do
  require "more vespene gas"
end
# 1 assertion, the same as should_raise(LoadError)

should_raise(:kind_of => ScriptError) do
  require "more vespene gas"
end
# 1 assertion, slightly less strict than with :instance_of (note: LoadError < ScriptError)

should_raise(:message => "no such file to load") do
  require "more vespene gas"
end
# 2 assertions

should_raise(:message => /vespene/) do
  require "more vespene gas"
end
# 2 assertions

should_raise(LoadError, :message => "such file to load") do
  require "more vespene gas"
end
# 2 assertions

should_raise(:kind_of => LoadError, :message => "file to load") do
  require "more vespene gas"
end
# 2 assertions

should_raise(:instance_of => LoadError, :message => "to load") do
  require "more vespene gas"
end
# 2 assertions

Conclusion

As you can tell, I’m eagerly awaiting Starcraft II.

No, I meant: check out the code on gist 20019. I’ve included a reasonable suite of unit tests in a comment at the end.

Feel free to use it in any way you like. Just make sure you don’t sue me.

git_remote_branch 0.2.6 is out!

I’ve just released a new and improved version of git_remote_branch. Code named 0.2.6!

Ok, I admit. I haven’t really begun using code names.

I’m promoting the project from a pre-alpha to an alpha release. There’s still a lot to do, but the stability and “testedness” have improved greatly. Following are both sides of the maturity story.

The project is maturing

  • grb got its first contributor, Caio Chassot
  • I added a lot of tests, both unit and functional
    • there might even be interesting stuff to see in there for those who need to test command-line tools
  • the gem can now be installed directly from RubyForge
  • git_remote_branch now has a Google Group

The project is still immature

  • it swears a lot
  • no rubyforge page, despite the project being on rubyforge (at rubyforge.org/projects/grb)
  • no real documentation other than running grb help
  • very little in code documentation. On the other hand the code is spectacularly clean and readable, so that’s completely unnecessary. Just kidding.

What’s new in 0.2.6?

Three new actual features

  • the ‘rename’ command, contributed by Caio Chassot
  • the ‘publish’ command
  • the −−silent option to completely mute grb output as well as every git command run by grb on your behalf

And other stuff

  • the grb bin file now works when symlinked (also thanks to Caio Chassot)
  • lots of unit and functional tests
  • bug fixes
  • more flexibility for running grb outside of a git repository (e.g. for ‘explain’ or ‘help’)
  • now officially under the MIT license
  • refactored a bunch of rake tasks

Git the new version

To install the newest version of the gem, simply run

sudo gem install git_remote_branch

If you really want to be on the bleeding edge you can also get it on GitHub. Note however that in ‘bleeding edge’ the word ‘bleeding’ is still the most important one at that point.

git clone git://github.com/webmat/git_remote_branch.git
cd git_remote_branch
rake install

The ‘install’ task will run the tests before installing so you’ll need Shoulda, mocha, redgreen and ruby-debug for that approach.

Not familiar with git_remote_branch?

What it is

The basic idea for git_remote_branch is to trivialize the interaction with remote branches. The first goal is to make the commands for the simple situations easy.

The secondary goal, is to help you learn the commands by seeing them displayed in a beautiful shade of red each time you use grb, along with git’s output.

git_remote_branch lets you

  • create local-remote branche pairs, and tracks the remote branch automatically (for automatic merges when you git pull)
  • publish a local branch as a remote branch, very similar to create
  • delete local-remote branch pairs
  • track a remote-only branch
  • rename a local-remote branch pair
  • explain by simply spitting out the necessary commands to do any of the above

How to use it

explain

If you simply want to use grb as a cheatsheet (and run nothing on your behalf), you can use the explain command:

$ grb explain create
git_remote_branch version 0.2.6

List of operations to do to create a new remote branch and track it locally:

git push origin current_branch:refs/heads/branch_to_create
git fetch origin
git branch −−track branch_to_create origin/branch_to_create
git checkout branch_to_create

or

$ grb explain create my_branch my_origin
git_remote_branch version 0.2.6

List of operations to do to create a new remote branch and track it locally:

git push my_origin current_branch:refs/heads/my_branch
git fetch my_origin
git branch −−track my_branch my_origin/my_branch
git checkout my_branch

Notice that you can specify any normally expected parameter you’d normally include and ‘explain’ will use them in the list of commands it suggests.

Even better, if you’re in your repository, the current branch is going to be taken into account:

(master) $ grb explain create my_branch my_origin
git_remote_branch version 0.2.6

List of operations to do to create a new remote branch and track it locally:

git push my_origin master:refs/heads/my_branch
git fetch my_origin
git branch −−track my_branch my_origin/my_branch
git checkout my_branch

Of course, ‘explain’ works for all commands: create, publish, delete, track and rename.

The main commands

I’m not going to painstakingly give an example for each command. I’ll only give two, to show how git’s responses are displayed when running grb without ‘explain’:

(master)$ grb create test_branch
git_remote_branch version 0.2.6

git push origin master:refs/heads/test_branch
Total 0 (delta 0), reused 0 (delta 0)
To git@github.com:webmat/git_remote_branch.git
 * [new branch]      master -> test_branch

git fetch origin

git branch −−track test_branch origin/test_branch

git checkout test_branch
Switched to branch "test_branch"

(test_branch)$ grb delete test_branch
git_remote_branch version 0.2.6

git push origin :refs/heads/test_branch
To git@github.com:webmat/git_remote_branch.git
 - [deleted]         test_branch

git checkout master
Switched to branch "master"

git branch -d test_branch

(master) $ 

Yes my friends, I have just boldly used grb on my real repository for your viewing pleasure.

But worry not, no repository was hurt during the writing of this article.

Feedback

For any feedback you’re of course welcome to

Thanks

The context

Recently at GiraffeSoft we started a new project, based on another existing project we already had going. We could call this a long term fork. Let me give you a little bit more context on the situation.

  • These projects will both keep being actively developed in the future;
  • They will have some fundamental differences that will not be reconciled;
  • They will however keep many similarities and we expect that they will benefit from the exchange of some specific patches, as development on both moves forward.

In days past, this problem could have been solved reasonably well by cloning the central repository and then exchanging patches and applying them manually.

As you’ve guessed already, we’ve decided to try using Git to help manage this long term relationship.

I must warn you however. It’s the first time we attempt keeping a long term fork like that. We’re not sure whether it’s going to be worth the trouble and whether the diverging of the two projects will eventually prevent us from efficiently benefiting from using Git to manage the exchange of patches. We’re not sure whether this is the best way to accomplish this either.

On one hand, all this trouble may or may not be worth the effort. On the other hand, all of this sounds like ultra elite Git-fu. Hence our decision to explore this approach.

Another less technical reasons was also at play in our decision. We wanted to have one wiki per project on GitHub :-)

What we’re going to do

So at first, we have a remote repository for the initial project. There are of course an arbitrary number of client side clones of the repository (what Subversion calls working directories). None of them will be affected in any way.

The first thing I will do is clone the initial project to a new client side repository. I’ll set this up in such a way that it won’t use the initial project as its default origin. On the other hand I’ll make sure it has one branch that interacts with the initial project for future patch exchanges.

Then from that client-side repository, I’ll initialize a new remote repository, which will serve as the default remote repository for the new project.

What we’ll end up with is 2 remote repositories which will have a lot in common but won’t be linked to one another in any way. It won’t be a GitHub fork, for instance.

There will be exactly one client side repository that knows about the two central repos and that can exchange commits between them. All other new client-side clones of the new project will be plain old regular Git clones.

It would be easy to set up more client side repositories to be aware of both repositories, but to me it doesn’t really seem necessary.

Article too long?

As with my previous articles about Git, I’ll provide detailed instructions for you to follow along on dummy repositories (if you’re interested). When you return to this article to attempt something similar, you may want to skip to the executive summary section, where I list strictly the important operations without all the rambling.

The instructions with the rambling

Setting up a dummy initial project

Find yourself a comfortable directory and run these few commands to initiate a dummy client-side repository and its corresponding dummy remote:

mkdir test; cd $_
mkdir initial_project initial_wd
GIT_DIR=initial_project/ git init

# Creating a dummy repo
cd initial_wd
git init
echo foo > file.txt
git add .
git commit -a -m "initial commit"
echo bar >> file.txt
git commit -a -m "modification"

# Setting up what's gonna be the central repo for our initial app
git remote add origin ../initial_project/
git push origin master

git config branch.master.remote origin
git config branch.master.merge refs/heads/master

The last 2 config commands configure your master branch to automatically track remote master when issuing the command ‘git pull’. In other words, it’s equivalent to running the command ‘git branch −−track master origin/master’, with the only difference being that ‘branch −−track’ is primarily intended to create a new local branch, whereas here we already have our local branch.

We can now check for the expected behavior:

#   mat@mm initial_wd (master)$ git pull
#   Already up-to-date.

Note also that here I simplify the instructions for following along by creating a dummy remote repository that’s in fact only in another local directory. If for example you wanted to use GitHub, your Git remote command would simply look like:

git remote add origin git@github.com:username/initial_project.git

The actual fork

# Create a directory to host the new remote repository
cd ..
mkdir project2
GIT_DIR=project2/ git init

Now we will use Git clone with the -o option. This lets us give the initial project another name than the default ‘origin’. We’ll want to use the name ‘origin’ for the new repository we’ll create to actually track the new project. I decided to name it the initial project’s origin ‘ip_origin’.

# Specify origin name, then the path to the shared repo
# and finally a directory name for the local working directory.
git clone -o ip_origin initial_project wd

Setting up the relationship with the initial repository

We first create a branch specifically to track the initial project’s master branch. Then we set it up to track the initial project.

cd wd
git branch ip_master
git config branch.ip_master.remote ip_origin
git config branch.ip_master.merge refs/heads/master

Setting up the relationship with the new project’s repository

git remote add origin ../project2
git push origin master

git config branch.master.remote origin
git config branch.master.merge refs/heads/master

Let’s pretend some new development happened

echo 'shareable modification' > shareable_file.txt
git add shareable_file.txt
git commit -m "shareable modification"

echo "specific to new project" > not_shareable.txt
git add not_shareable.txt
git commit -m "specific to new project"

So I have now begun working on the new project and I already have one commit that could benefit the initial project as well. The progress looks a little like this:

A look at gitk’s representation of the new development

So I switch to the branch that manages the relationship with the initial project and I pick the commit before the last one in master.

git checkout ip_master
git cherry-pick master^

Gitk, after cherry-picking one commit

Everything’s dandy so far, except for the subtle fact that I have brought us all to the edge of a cliff.

If I tried to push to the initial repository right now, I’d be in for a nasty surprise:

#   mat@mm wd (ip_master)$ git push
#   Counting objects: 7, done.
#   Compressing objects: 100% (4/4), done.
#   Writing objects: 100% (6/6), 564 bytes, done.
#   Total 6 (delta 1), reused 0 (delta 0)
#   Unpacking objects: 100% (6/6), done.
#   To /Users/mat/blog/long-term-fork/test/initial_project
#      7178a89..3ca0240  master -> master

Oops! By default, ‘git push’ syncs up all branches of the same name with the current branch’s origin. So that would push the new project’s master branch on our initial project’s shared master.

This is obviously not what we want. We only want ip_master to be pushed to the initial project’s master.

Trying to remember to always explicitly run ‘git push ip_origin ip_master:master’ wouldn’t do it for me. To keep the analogy, that would be akin to doing a cartwheel on the edge of said cliff: a lot of fun until you make a mistake. So obviously we’d like a simple ‘git push’ to do the right thing.

So here’s how we configure it:

git config remote.ip_origin.push refs/heads/ip_master:master

Now we can safely issue the ‘git push’ command from ip_master and have Git push ip_master to ip_origin/master.

git push
#   Counting objects: 4, done.
#   Compressing objects: 100% (2/2), done.
#   Writing objects: 100% (3/3), 310 bytes, done.
#   Total 3 (delta 0), reused 0 (delta 0)
#   Unpacking objects: 100% (3/3), done.
#   To /Users/mat/blog/long-term-fork/test/initial_project
#      027a48f..9db6c3f  ip_master -> master

There’s now only one remaining annoyance we’re not yet protected against. If new branches are created in the initial project’s central repository, a ‘git pull’ when standing in the ip_master branch would pull them all in our new project’s working directory. I consider this one only an annoyance, since I could just decide not to pay attention to them. On the other hand they will be distracting and may also clash with the branches I create for the development on my new project. So we want to avoid that behavior as well.

To better understand the current Git configuration, let’s have a look at .git/config:

  ...
  [remote "ip_origin"]
    url = /Users/mat/blog/long-term-fork/test/initial_project
    fetch = +refs/heads/*:refs/remotes/ip_origin/*
  [branch "master"]
    remote = origin
    merge = refs/heads/master
  [branch "ip_master"]
    remote = ip_origin
    merge = refs/heads/master
  [remote "origin"]
    url = ../project2
    fetch = +refs/heads/*:refs/remotes/origin/*

So in both ‘remote’ sections I can see that fetch is configured to bring everything locally (the * wildcards).

So here’s how I limit what gets pulled when I pull from the initial project’s repository.

git config remote.ip_origin.fetch +refs/heads/master:refs/remotes/ip_origin/master

The part before the colon is the name of the interesting branch on the remote server. The part after the colon is your local Git repo’s internal branch, used to track the remote branch (not to be confused with our user branch ip_master).

Setting up remote branches on both projects, pulling, pushing and exchanging more commits is left as an exercise to the reader.

Conclusion

So that’s the gist of it, my friends. I am basically set up to work on my new project like I would in a more typical situation. I also have a special branch set up to interact with the initial project. With this branch I’ll be able to do two things:

  • Pull new developments from the initial project and then cherry-pick only the shareable commits into the new project’s other branches.
  • Cherry-pick in the other direction to bring certain commits from the new project into this branch and then push them up to the initial project.

For this approach to be useful however, we’ll have to make sure we create as concise commits as possible. Gone are the days of committing a whole afternoon in one meaningless commit containing 12 different modifications.

Of course coding sprees of a couple hours are not out of the question. Features like ‘git add −−patch’ are a great help when comes time to extract meaningful commits out of the result of a few hours of intense coding. For a good introduction to −−patch (and a few other powerful features), be sure to read Ryan Tomayko’s The Thing about Git.

The executive summary

So let’s reiterate strictly the interesting bits necessary to set up a long term fork when starting a new project from an existing one.

We already have:

  • the initial project’s repository at url git@github.com:username/initial_project.git
  • the new project’s empty repository, also created at git@github.com:username/new_project.git
# Create new local clone for the new project
git clone -o ip_origin git@github.com:username/initial_project.git new_project
cd new_project

# Set up a standard track between initial master and local ip_master
git config branch.ip_master.remote ip_origin
git config branch.ip_master.merge refs/heads/master

# Automatically push the right branch
git config remote.ip_origin.push refs/heads/ip_master:master
# Don't bring in the other shared branches from initial project
git config remote.ip_origin.fetch +refs/heads/master:refs/remotes/ip_origin/master

# Push new local repo it to new shared repo
git remote add origin git@github.com:username/new_project.git
git push origin master

# Configure standard track of master with new local repo
git config branch.master.remote origin
git config branch.master.merge refs/heads/master

That’s it! We now only have to cherry-pick like there’s no tomorrow.

Working with new people often influences the way you work. The influences can range from picking up simple tricks to seeing fundamental facts about your craft in a new light.This week when I began working with James I saw him run his tests directly from TextMate. Of course I knew it was possible to run Ruby from TM, including tests.For some inexplicable reason however I had never bothered to try it. This trick is very convenient for two reasons: TextMate cleans up the backtraces and resolves each level of the trace to a clickable link to your code file. So I decided to include this trick in my workflow.I encountered two problems with this however. Two gems I usually use in my tests don’t play well with running tests from TextMate.

redgreen

The redgreen gem highlights the . F and E (among other bits) in your test runs with green, red and yellow. When running my whole test suite it’s something I want to have. It’s not only visually pleasing, but it also lets me see at a glance whether any problems were encountered.When run from TextMate, tests using redgreen are displayed without having the console coloring stripped out, which gives something like this:

The result of running tests from TextMate, when using redgreen

Riiiight.For the record, here’s the nice result when run at the console:

Running tests with redgreen, from the console

quietbacktrace

The other gem I can’t do without is James Golick and Dan Croak’s quietbacktrace. This gem lets you specify filters and silencers to clean up those huuuuuge Rails or Merb backtraces.Filters let you remove useless parts of a given line in the backtrace, such as the path leading to your gems. Silencers let you completely remove some lines from the backtrace, such as all the lines referring to what happened inside Rails, leading to your error. The most popular filters and silencers are already provided with quietbacktrace, as you’ll see below.So the result, of course is that messing with backtraces breaks TextMate’s ability to link a backtrace line with the corresponding file.

A simple snippet to fix this

Since I didn’t really want to do away with these tools, I included a bit of a hacky snippet in my test_helper.rb file to detect whether a test run was happening in TextMate or at the console.If you find yourself in the same fix as me, feel free to use the following.Among your requires:

IN_TM = !ENV['TM_DIRECTORY'].nil?
unless IN_TM
  require 'redgreen'
  require 'quietbacktrace'end

And then:

class Test::Unit::TestCase
  unless IN_TM
    self.backtrace_silencers << :rails_vendor
    self.backtrace_filters   << :rails_root
  end
  #...
end

Now I can have my testing niceties when running my tests from the console and they don’t break the TextMate integration :-)

Trying this out for the first time?

If like me you just decided to try this out for the first time, the shortcuts are easy to remember. Command-R runs any Ruby file (in this case, the whole test file) and Command-Shift-R runs the single test the cursor is in.There’s a little hiccup in sight, however. If you’re using Rails 2+, you have to apply a very small fix to TextMate before you’ll be able to run your tests. Read more about the fix on Marc-André Cournoyer’s blog.