I have been writing a lot of tests for our API lately, and I wanted to share a technique that has helped me build a test suite that is easy to understand and write. Our API keeps a pretty consistent pattern which makes it a perfect candidate for RSpec shared examples. Similar to RSpec shared context which I discussed in another post, shared examples are a great way to clean up test code and create useful abstractions.
To include a shared example in a spec you have to use one of the following methods:
1234
include_examples"name"# include the examples in the current contextit_behaves_like"name"# include the examples in a nested contextit_should_behave_like"name"# include the examples in a nested contextmatchingmetadata# include the examples in the current context
These methods allow you to pass parameters that can be used in the example group. I utilize this for my api tests to provide a factory name, the path for the request and the model:
123456789
# /spec/requests/account_spec.rbrequire'rails_helper'describe'Account API'dodescribe'/api/accounts'doit_behaves_like'an index route',Account,:account,'/api/accounts'endend
In a Rails project, it is usually recommended to include shared examples under the support folder, so that they are autoloaded and available to use in all your tests.
123456789101112131415
#/spec/support/example_groups/index_routeshared_examples"an index route"do|model,factory,path|let(:resources){model.to_s.tableize}it"responds with a collection of the resource",type::requestdoresource=FactoryGirl.create(factory)getpathjson=JSON.parse(response.body)expect(response.status).toeq200expect(json[resources].length).toeq1endend
Now when I want to test another API endpoint that follows the RESTful index pattern, I can reuse this spec. Another benefit is that if I were to change the behavior for the index action by requiring an oauth token, I would only need to make the change in one place. Shared examples are also composable, so you could create a shared example for a restful resource by combining shared examples for all the different route types:
1234567
shared_examples"a RESTful resource"do|model,factory,path|it_behaves_like"an index route",model,factory,pathit_behaves_like"a create route",model,pathit_behaves_like"a show route",model,factory,pathit_behaves_like"an update route",model,factory,pathit_behaves_like"a destroy route",model,factory,pathend
One of my big breakthroughs in understanding object oriented design in Ruby came when I learned about duck typing. Wikipedia provides a nice definiition of the concept:
duck typing is a style of typing in which an object’s methods and properties determine the valid semantics, rather than its inheritance from a particular class or implementation of an explicit interface.
This means that in Ruby it does not matter if an object is a specific type or class, only that it can respond to a given message. For example, in the stringify method below, it does not matter if the argument is an integer or symbol or some other class, only that it can respond to the to_s method.
def stringify(a)
a.to_s
end
stringify(1)
=> "1"
stringify(:hello)
=> "hello"
class NewClass
def to_s
"works for custom objects too!"
end
end
stringify(NewClass.new)
=> "works for custom objects too!"
Not having to care about a specific object’s class enables a lot of flexiblity for swaping out collaborator objects. To illustrate this, let’s take a look at an example:
File Storage with Duck Typing
My app generates reports, and I would like to save these reports. Initially, I just want to save the reports to my local disk.
12345678910111213141516171819202122
classReportdefinitialize(name,data)@name=name@data=dataenddefsave_to(storage)storage.store(@name,@data)endendclassFileStoragedefstore(file_name,data)File.open("reports/#{file_name}",'w')do|file|file.write(data)endendendreport=Report.new('Sales Figures','sales data blah...')file_store=FileStorage.newreport.save_to(file_store)
Later, I decide that it would also be nice to have the ability to upload reports to S3. With duck typing, implementing this new feature is as simple as creating a new class for uploading to S3 which implements a store method:
12345678910111213
classS3Storagedefinitialize(bucket_name)@bucket=AWS::S3.new.buckets[bucket_name]enddefstore(file_name,data)@bucket.objects.create(file_name,data)endendreport=Report.new('Sales Figures','sales data blah...')s3_store=S3Storage.new('my_bucket')report.save_to(s3_store)
Start thinking about objects in terms of the messages they receive, and you will be rewarded with the flexibility that duck typing allows.
Do you ever find that many of your specs have a similar setup that you are repeating over and over? I often find this happening when I’m writing feature specs that require a logged in user. For example:
1234567891011
feature'some super cool feature'dobackground'login an admin'douser=create(:user,role:'admin')user.confirm!login_as(user,scope::user)endscenario'my super cool scenario'do...endend
If my login or authentication process changes, I have to manually go into each spec and change the code. I also find that this setup code clutters the spec and makes it harder to read. If we could extract the setup code and replace it with an easy to read single line, it would solve both of these problems.
RSpec shared contexts provide just this type of solution. Using a shared context we can extract the setup into its own file, and rewrite our feature spec in a cleaner way:
1234567
feature'some super cool feature'doinclude_context'a logged in admin'scenario'my super cool scenario'do...endend
To create a shared context:
123456789
# ./spec/support/shared_contexts/a_logged_in_admin.rb shared_context'a logged in admin'dobefore'create and login admin'douser=create(:user,role:'admin')user.confirm!login_as(user,scope::user)endend
Since all files located in spec/support are automatically required in the default spec_helper.rb, we can call the context from any spec. Now when we need to change how a user is logged in, we just have to change the code in our shared context and without having to change our specs.
As the size and complexity of the applications I have been working on has grown, the importance of writing good unit test has become increasingly apparent. The goal is to have a test suite that is fast and able to withstand changes.
She has a rule about testing outgoing command messages that says, “expect to send outgoing command messages”. This means that we should assert that the method actually issues the command message, but we don’t assert the result of that message.
For example, suppose I have a party, and I want users to be able to RSVP:
I want to assert that when user.rsvp(party) is called, the add_guest message is sent to the party object. Mocks are the perfect tool for testing these command messages because they allow us to test that the correct message is being passed without worrying about how the other object actually deals with that message:
12345678910
describeUserdodescribe'#rsvp'doit"tells the party to add the user as a guest"douser=User.newparty=double('party')expect(party).toreceive(:add_guest).with(user)user.rsvp(party)endendend
This allows us to test the rsvp method without having to actually deal with the Party class’ implementation of add_guest. But what happens if we change the public interface for add_guestto allow a guest specify how many to friends they plan to bring to the party?
Our rsvp method is now broken because the add_guest method requires two arguments, but we are only passing it one. Unfortunately our test for rsvp still passes because our test double does not reflect the actual interface that it is mocking.
To solve this problem I recently started using a mocking framework called Bogus. It ensures that your test doubles have the same interface as the real class.
123456789101112
require'bogus/rspec'...describe'#rsvp'doit"tells the party to add the user as a guest"douser=User.newparty=fake(:party)user.rsvp(party)expect(party).tohave_received.add_guest(user)endend...
Now when we run the test we receive the following failure:
123456
Failures:1)User#rsvp tells the party to add the user as a guestFailure/Error:party.add_guest(self)ArgumentError:wrongnumberofarguments(1for2)
Great! Our test double now implements the same interface as the actual Party class and alerts us that our rsvp method is not passing the correct number of arguments. Let’s update rsvp:
Failures:1)User#rsvp tells the party to add the user as a guestFailure/Error:expect(party).tohave_received.add_guest(user)ArgumentError:triedtostubadd_guest(user,number_of_companions)witharguments:#<User:0x007fa49210caa0>
Whoops! Looks like our stub for add_guest doesn’t implement the correct interface for add_guest. Let’s update it:
123456789
...describe'#rsvp'doit"tells the party to add the user as a guest"do...expect(party).tohave_received.add_guest(user,0)...Finishedin0.006seconds1example,0failures
Another great feature of Bogus is that it supports mocking a duck type. For example, suppose that in addition to parties, we want users to be able to rsvp to dinners. We can implement this with a duck type by creating an add_guest method for our new Dinner class:
And now in our test we can mock our duck type interface:
12345678910
describeUserdodescribe'#rsvp'doit"tells the guestable to add the user as a guest"douser=User.newguestable=fake(:guestable){[Party,Dinner]}user.rsvp(guestable)expect(guestable).tohave_received.add_guest(user,0)endendend
Mocks are a great way to focus on the messages that your objects are sending when testing. Give them a try and experience the benefits of a faster more resilient test suite.
Rake is something I have been using since day one with Rails, but that I really haven’t gone any deeper with than the basic rails rake tasks. Today I had to write a rake tasks that ran some checks and updated records.
Here’s what I learned:
This article by Jason Seifer was a great introduction to rake. The big idea is that rake has namespaces and inside those namespaces you define tasks.
namespace :feed do
desc "Feed dog"
task :dog => :environment do
puts "fed the dog"
end
desc "Feed cat"
task :cat => :environment do
puts "fed the cat"
end
desc "Feed dog and cat"
task :all => [:dog, :cat]
end
Rake tasks should be tested. Thoughtbot provides a really nice shared context that makes testing very easy.
I have been hearing a lot recently about continuous deployment especially in the context of building a lean startup, so I decided to try and get one of my projects running with continuous deployment.
Here’s what I learned today:
Travis CI is an open source tool for continuous integration and deployment.
There are several steps to setting up your project with Travis CI:
Travis CI integrates nicely with Heroku, so that your code be deployed after a successful build. I was having some problems deploying using Anvil, so I switched to using git. Here is what I added to my .travis.yml for deploying to Heroku:
Prior to today, I knew database indexes were something you used to help speed up a query, but I didn’t really understand their importance. I read this great Thoughtbot blog to learn more about them.
Here is what I learned today:
When you perform a database query on a column without an index the database will look at each row and compare its value with the query value. If you don’t have many records this isn’t a big deal, but when you are talking about thousands or millions of rows that is a lot of work! Indexes provide a quick reference for the database, so it can retreive the matching records without having to look at every row.
If you are using PostgreSQL (and apparently most other SQL databases), the primary key is index automatically. This means that in Rails the “id” column is indexed automatically because Rails tells the database the “id” column is the primary key.
Basically any column you have to query for a set of records should be indexed.
There are two downsides to adding an index. First, anytime you INSERT or UPDATE, the database must write both the record and the index reference which means more work for the database. Second, if you are creating an index for an existing set of records it could take a while. The Thoughtbot blog talks about a database migration that took 7 hours!
They say starting is the hardest part. My initial idea for this blog is to post the things I learned during the day and the resources that helped me learn them.
Here is what I learned today:
You can host a blog on Github using Github Pages. There is a framework called Octopress to help make things easy. I followed the documentation for initial setup and launched this blog.
Deploying the changes for the blog can be done with the following steps: