Clean RSpec configuration directory structure for Ruby on Rails gems needed in testing
When your Ruby on Rails project is getting bigger your test suite as well. You need to test more of your business logic and sometimes you will use other gems that can help you with that. Most of the time you may need something like database_cleaner
, capybara
for feature tests or rspec-sidekiq
to test your workers.
Adding new gems needed for testing often requires changes in RSpec configuration. You add a new line of config here and there in the spec_helper.rb
or rails_helper.rb
file and suddenly you have huge and hard to understand config file for RSpec.
I will show you how I organize my RSpec configuration directory structure to easily add or modify the RSpec configuration in a clean way.
Prepare RSpec config directory structure
I keep all of my configuration code related to RSpec in directory spec/support/config
. You can create it.
Next step is to ensure the RSpec will read files from the config directory. In order to do it please ensure you have below line in your spec/rails_helper.rb
file.
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
By default, it is commented out so please uncomment it.
Separate config files for different testing gems
Here I will show you examples of popular gems and how to keep their configuration clean. A few examples are on the video and many more code examples are in this article.
Configuration for Database Cleaner
For database_cleaner gem you can just create config file spec/support/config/database_cleaner.rb
:
# spec/support/config/database_cleaner.rb
require 'database_cleaner'
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :deletion
DatabaseCleaner.clean_with(:deletion)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
Configuration for Capybara
Here is my configuration of Capybara placed at spec/support/config/capybara.rb
.
# spec/support/config/capybara.rb
require 'capybara/rspec'
JS_DRIVER = :selenium_chrome_headless
DEFAULT_MAX_WAIT_TIME = ENV['CI'] ? 5 : 2
Capybara.default_driver = :rack_test
Capybara.javascript_driver = JS_DRIVER
Capybara.default_max_wait_time = DEFAULT_MAX_WAIT_TIME
RSpec.configure do |config|
config.before(:each) do |example|
Capybara.current_driver = JS_DRIVER if example.metadata[:js]
Capybara.current_driver = :selenium if example.metadata[:selenium]
Capybara.current_driver = :selenium_chrome if example.metadata[:selenium_chrome]
Capybara.default_max_wait_time = example.metadata[:max_wait_time] if example.metadata[:max_wait_time]
end
config.after(:each) do |example|
Capybara.use_default_driver if example.metadata[:js] || example.metadata[:selenium]
Capybara.default_max_wait_time = DEFAULT_MAX_WAIT_TIME if example.metadata[:max_wait_time]
end
end
I have a few custom things here like easy option to switch between different browsers executing my tests by just adding tag like :selenium_chrome
to test:
# spec/features/example_feature_spec.rb
it 'something', :selenium_chrome do
visit '/'
expect(page).to have_content 'Welcome'
end
You can learn more how to configure Capybara with Chrome headless here.
Configuration for Sidekiq
I like keep my Sidekiq configuration with other useful Sidekiq related gems in one place spec/support/config/sidekiq.rb
:
# spec/support/config/sidekiq.rb
require 'rspec-sidekiq'
require 'sidekiq/testing'
require 'sidekiq_unique_jobs/testing'
RSpec.configure do |config|
config.before(:each) do
Sidekiq::Worker.clear_all
# https://github.com/sidekiq/sidekiq/wiki/Testing#testing-worker-queueing-fake
if RSpec.current_example.metadata[:sidekiq_fake]
Sidekiq::Testing.fake!
end
# https://github.com/sidekiq/sidekiq/wiki/Testing#testing-workers-inline
if RSpec.current_example.metadata[:sidekiq_inline]
Sidekiq::Testing.inline!
end
end
config.after(:each) do
if RSpec.current_example.metadata[:sidekiq_fake] || RSpec.current_example.metadata[:sidekiq_inline]
Sidekiq::Testing.disable!
end
end
end
RSpec::Sidekiq.configure do |config|
config.warn_when_jobs_not_processed_by_sidekiq = false
end
I use gems like sidekiq-unique-jobs and rspec-sidekiq. Here you can read more about testing Sidekiq.
Configuration for FactoryBot (known as FactoryGirl)
FactoryBot config file for Ruby on Rails can be isolated at spec/support/config/factory_bot.rb
:
# spec/support/config/factory_bot.rb
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
Configuration for JSON Spec
If you have API endpoints in your application you may like the json_spec gem that can help you test JSON responses. Here is my config at spec/support/config/json_spec.rb
# spec/support/config/json_spec.rb
require 'json_spec'
RSpec.configure do |config|
config.include JsonSpec::Helpers
end
Configuration for RSpec Retry
Sometimes big projects have painful tests that randomly fail. The last rescue when we cannot make them stable and always green is to retry them a few times before marking them as failed. This is one option how to deal with flaky tests and we can use for that rspec-retry gem.
# spec/support/config/rspec_retry.rb
RSpec.configure do |config|
# show retry status in spec process
config.verbose_retry = true
# show exception that triggers a retry if verbose_retry is set to true
config.display_try_failure_messages = true
# run retry only on features
config.around :each, :js do |ex|
# retry test 3 times on CI but do not retry when testing locally
ex.run_with_retry retry: (ENV['CI'] ? 3 : 1)
end
# callback to be run between retries
config.retry_callback = proc do |ex|
# run some additional clean up task - can be filtered by example metadata
if ex.metadata[:js]
Capybara.reset!
end
end
end
Here you can learn more about how to deal with flaky tests:
Configuration for Shoulda Matchers
Shoulda Matchers provides RSpec and Minitest-compatible one-liners that test common Rails functionality. Here is my config spec/support/config/shoulda_matchers.rb
:
# spec/support/config/shoulda_matchers.rb
require 'shoulda/matchers'
Shoulda::Matchers.configure do |config|
config.integrate do |with|
# Choose a test framework:
with.test_framework :rspec
#with.test_framework :minitest
#with.test_framework :minitest_4
#with.test_framework :test_unit
# Choose one or more libraries:
#with.library :active_record
#with.library :active_model
#with.library :action_controller
# Or, choose the following (which implies all of the above):
with.library :rails
end
end
Configuration for VCR and WebMock
You can record your requests in testing with VCR or mock request with WebMock. This is my config spec/support/config/vcr.rb
:
# spec/support/config/vcr.rb
require 'vcr'
VCR.configure do |config|
config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
config.hook_into :webmock
config.allow_http_connections_when_no_cassette = true
config.ignore_hosts(
'localhost',
'127.0.0.1',
'0.0.0.0',
'example.com',
)
end
WebMock.disable_net_connect!(allow: [
'example.com',
])
Configuration for Knapsack Pro to split test suite across parallel CI nodes
If you have a large test suite taking a dozen of minutes or maybe even hours then you may want to run tests in parallel across multiple CI node with Knapsack Pro to get the fastest CI build.
This is example config file.
# spec/support/config/knapsack_pro.rb
require 'knapsack_pro'
KnapsackPro::Adapters::RSpecAdapter.bind
Here you can learn more about how it works.
Configuration for Page Objects
You can keep your page objects in a separate directory. Page objects can be later reused in multiple tests.
Create directory spec/support/page_objects
. Here is example page object for billing page:
# spec/support/page_objects/billing_page.rb
class BillingPage
def self.fill_company_details(
first_name: 'Kayleigh',
last_name: 'Johnston',
company: 'Example Company',
vat_id: 'UK1234567890',
email: 'kayleigh.johnston@example.com',
street_address: '59 Botley Road',
locality: 'Midtown',
region: '', # can be blank
postal_code: 'IV27 5LL',
country_name: 'United Kingdom',
website: 'http://example.com'
)
Capybara.fill_in 'first_name', with: first_name
Capybara.fill_in 'last_name', with: last_name
Capybara.fill_in 'company', with: company
Capybara.fill_in 'vat_id', with: vat_id
Capybara.fill_in 'email', with: email
Capybara.fill_in 'street_address', with: street_address
Capybara.fill_in 'locality', with: locality
Capybara.fill_in 'region', with: region
Capybara.fill_in 'postal_code', with: postal_code
Capybara.select country_name, from: 'country_name'
Capybara.fill_in 'website', with: website
end
end
Then you can use the page object in your feature spec.
# spec/features/billing_spec.rb
it 'fills company details on billing page' do
BillingPage.fill_company_details
end
Configuration for shared examples
You can organize your RSpec shared examples in directory spec/support/shared_examples
that will be autoloaded.
Summary
As you can see there are a lot of different useful gems for testing in RSpec. If we would keep all their configuration just in spec_helper.rb
we would quickly get a messy file. Separation of config files can help us keep it clean and easy to maintain. Let me know in comments how you keep your config files sane.