Recently I had posted about a few testing strategies that can be applied with RSpec. One of the patterns I mentioned was using something like NullDB to ensure your unit tests were not hitting the database. I had a few conversations about what I’d written, notably from my colleague Ian Lesperance. We discussed, and I conceded, that it’s preferable to have tests related to one class in one spec file. In particular I had split out the tests for the unit level and the integration with the database tests. So, here are my experiments on how I brought those tests back while keeping the same integrity of using a database for some test and forcing the null object pattern on other tests.
I had some issues having those tests in the same file, but with a little help from another colleague, JT Archie, we managed to figure it out.
Consider this rspec test:
describe Widget do
describe "#higest_selling", :db do
it "uses the 'highest_selling' scope" do
...
end
end
describe "#display_name" do
it 'concats the widget name and manufacturer' do
...
end
end
end
The ‘higest_selling’ method is a scope and has the ‘:db’ tag associated to the block, while the ‘display_name’ test has no tags applied. I wanted this to be the case, no tags means no database but if you want to hit the database, you need to explicitly call it out.
One trick you might have missed above was no longer needing to do ‘db: true’ in the RSpec tag. With the following setting in the spec helper, you can apply a symbol directly like ‘:db’.
config.treat_symbols_as_metadata_keys_with_true_values = true
Testing with NullDB
To get this working, I had to use the HEAD revision of NullDB:
gem 'activerecord-nulldb-adapter', git: 'git://github.com/nulldb/nulldb.git'
Using NullDB within the same file, we can use the ‘nullify’ and ‘restore’ helpers, but I found it worked best using the ‘around’ configuration. Using ‘before’ and ‘after’ I was having issues with changing the connection adapter during a transaction. This way, it appears to get around that issue.
We run the configuration block around each test that has the ‘type: :model’ tag. RSpec-Rails applies these automatically to any tests in the ‘spec/models’ directory. We look to see if the example has the ‘:db’ tag and if it does, we restore the default connection adapter, and run the example. If the example does not have the ‘:db’ tag applied, we apply the NullDB adapter, run the example and then restore the default adapter.
Within the ‘spec_helper.rb’ file:
config.around(:each, type: :model) do |example|
if example.metadata[:db]
NullDB.restore
example.run
else
NullDB.nullify
example.run
NullDB.restore
end
end
Testing using stubs
There are other options and with a sizable amount of help from JT, we created a simple way to achieve a similar outcome. Under ActiveRecord there are two methods which actually hit the database, ‘exec’ and ‘exec_query’. These methods can be stubbed out much like any method on any object in an application codebase.
In the ‘spec_helper’ file, we replace the NullDB configuration with the following. We again check for the ‘db’ tag and if it’s not there we stub ‘exec’ and ‘exec_query’.
config.around(:each, type: :model) do |example|
unless example.metadata[:db]
ActiveRecord::Base.connection.stub(:exec).
and_raise("You're not allowed to do that")
ActiveRecord::Base.connection.stub(:exec_query).
and_raise("You're not allowed to do that")
end
end
Testing using Nosql
We took this concept one step further and created a Gem that wasn’t RSpec specific. We couldn’t believe our luck when RubyGems showed there was no Gem called ‘nosql’, so with that problem solved we created the Nosql gem. When included in a test suite, any call to the database will raise an exception.
With the around configuration block Nosql is disabled and enabled accordingly.
config.around(:each, type: :model) do |example|
if example.metadata[:db]
Nosql::Connection.disable!
example.run
else
Nosql::Connection.enable!
example.run
Nosql::Connection.disable!
end
end
All three of these options force unit tests to not hit the database. Database calls will either be ignored (NullDB), or will raise an error (Nosql). This should result in decreased execution time for tests as it will encourage the developer to stub out those interactions with the database.
About the Author