These are some tricks, tips, strategies, lessons learnt from a year of working with RSpec on Rails projects. Rails convention over configuration has proved fruitful for adoption and standardisation but there times when you do want to tweak certain things. We’ll highlight some ways how that can be done such as changing the directory structure of your tests.
RSpec Directory Structure
Coming from a Java background, I was used to see all of a projects tests in the ‘test’ directory with subdirectories off of those for certain types of tests such as ‘unit’ and ‘integration’. Using libraries like rspec and rspec-rails sets certain expectations on where certain types of tests will be. Model tests will be in ‘spec/models’, controllers in ‘spec/controllers’. This has been great importing useful helpers for those given situations, but it doesn’t tell me if those are unit tests or something else.
From rspec-rails 2.4 tags such as “type: :controller” can be used to import the appropriate helpers and the tests can be placed where they fit best for the application. I would recommend the following:
spec/ ./unit ./models ./controllers ./etc... ./integration ./models ./etc... ./acceptance
The spec files themselves would look like:
describe FooModel, type: :model do … end
RSpec 2.12 (current latest stable) supports tags for model, controller, helper and routing.
Unit testing without a database
Why is it important to do unit testing without hitting a database? If a test hits a database then the execution path is through your business logic, through a persistence library, perhaps a database adapter and then the database. A unit test should be focused on the smallest unit of execution, namely the response and messages sent by a class method. Besides testing this whole stack, there typically tends to be an order of magnitude more unit tests in a project than any other type of test and if the tests are hitting the database hundreds or thousands of times this will eventually lead to a slow test suite.
One way of ensuring unit tests do not hit a database in Rails is to use a null object pattern database adapter. The best known is nulldb. Although the latest stable release doesn’t support Rails 3.1+ adding the HEAD sha as a dependency will support the very latest Rails projects. The only place an application should really be hitting the database is within the application models so the nulldb adapter can be set for those types of tests.
gem 'activerecord-nulldb-adapter', git: 'git://github.com/nulldb/nulldb.git'
RSpec.configure do |config| ... config.before(type: :model) do require 'nulldb_rspec' ActiveRecord::Base.establish_connection :adapter => :nulldb end ... end
If an application is integrating with a software as a service the result is the same. Unit tests should not be hitting that API, stubs should be used to mock the dependency. One way to ensure the tests are not hitting an API would be to open the class that hits the API and overwrite any methods that the application uses and raise an exception.
class ApiClient def do_something raise “Whoa there, you shouldn’t be here!” end end
If an application does need to sometimes hit that API and sometimes not, RSpec allows models to be included depending on the tag. This would allow some test groups to use the API and others to be intercepted and raise the unexpected visitor error.
config.include ApiInterceptors, group: :model
What’s in an integration test
The integration tests are the best place for testing database integrations like scopes, or complex queries. They are also a good place for testing integrations with other libraries or services such as a message queue, background worker or remote API. An application may also want to test that the layers of an application are integrated correctly. After all with all that stubbing in the unit tests it can mislead a developer as to the correctness of an application. If an application has observers or is event driven this would be a good point to see if those layers are integrated successfully.
Sharing behaviours for integration and acceptance testing
If a project is using the BDD language in stories, the application may be reusing those in its acceptance tests. These stories are often the application codified in human language. Where they are used in the application they are often used in the acceptance testing stage but it doesn’t have to be constrained to there. If those stories are written without specifying the how it can be easier to share, e.g. “a user creates a foo widget” rather than “a user clicks on the ‘create foo widget’; a user fills in ‘bar’ for the foo widget; a user presses the create button’.
RSpec has a concept of ‘shared examples’ which can be used to share behaviours between testing stages. If the test reads in such a way where it could be run against a browser, command line or API, all of those test layers could use the behaviour.
shared_examples "foo" do describe "user creates a foo” do it "lists new foos" do given_user_exists when_user_creates_a_foo then_foo_is_listed_against_user end end end
Then in the target test file, including the shared example will bring that test in and be run in that context with associated tags.
require 'spec_helper' def given_user_exists end def when_user_creates_a_foo end def then_foo_is_listed_against_user end describe 'Foo', type: :foo do include_examples "a foo" end
The method calls can be defined so they are inline and in scope, this way each file has its own implementation of the requirement methods for the test. The methods can also be extracted into a module and included through RSpec configuration.
config.include(IntegrationHelpers, type: :foo)
module IntegrationHelpers def given_user_exists end def when_user_creates_a_foo end def then_foo_is_listed_against_user end end
Following this pattern means the business language can be added to code and used in multiple places, and therefore stages of the testing.
About the Author
BiographyMore Content by Robbie Clutton