Writing automated tests for your code should be a big part of your workflow. Luckily, testing Rails engines is not that different from testing regular Ruby on Rails applications. That’s why we will be focusing on the tricks you need to know to test your engines properly, and not the science of writing tests - there are tons of great books written on the topic.
But before we begin, let’s create a new branch for this chapter:
git checkout -b Chapter-6
Every developer has a favorite testing environment. Some people love factories, others prefer fixtures. We’re going to show you one way to test by using the tools we’re used to. You should adapt the following to your liking.
First, let’s add our dependencies to the gemspec
file in the Core
module. Note that since we’ll use those only for tests, we can add them as development dependencies.
blast_crm/engines/core/blast_core.gemspec
.
.
.
spec.add_development_dependency 'sqlite3', '~> 1.4.1'
# We're adding bootsnap here because it's a dependency of our parent
# and we'll need it to interact with it when running our tests
spec.add_development_dependency 'bootsnap', '>= 1.1.0'
spec.add_development_dependency 'database_cleaner', '~> 1.7.0'
spec.add_development_dependency 'factory_bot_rails', '~> 5.0.2'
spec.add_development_dependency 'faker', '~> 1.9.3'
spec.add_development_dependency 'rspec-rails', '~> 3.8.2'
end
As you can see, we’re going to be using RSpec, factory_bot, faker and Database Cleaner.
Don’t forget to run bundle install
, but from the Core
engine this time.
To generate the test database, run the following command from the parent application folder:
RAILS_ENV=test rake db:create && rake db:migrate
When you see the below output, you know the test database was created successfully:
Created database 'db/test.sqlite3'
Navigate to the Core
engine folder and run this command to generate the RSpec files:
rails g rspec:install
The below output shows us the files that were created:
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
rails_helper.rb
fileSince we are using RSpec inside an engine, we need to tweak the rails_helper.rb
file a little. You will find the updated file in Listing 2 below, but before updating yours, let’s go through the various changes:
factory_bot_rails
, faker
and database_cleaner
.
Core
path helpers.
core/spec/rails_helper.rb
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
# Use the appropriate path here
require File.expand_path("../../../../config/environment", __FILE__)
if Rails.env.production?
abort('The Rails environment is running in production mode!')
end
require 'rspec/rails'
# Require our dependencies
require 'factory_bot_rails'
require 'database_cleaner'
require 'faker'
# Set the ENGINE_RAILS_ROOT variable
ENGINE_RAILS_ROOT = File.join(File.dirname(__FILE__), '../../')
# Requires supporting Ruby files with custom matchers and macros, etc,
# from spec/support/ and its subdirectories.
Dir[File.join(ENGINE_RAILS_ROOT, 'core/spec/support/**/*.rb')].each do |f|
require f
end
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
RSpec.configure do |config|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
# Define how we want Database Cleaner to work
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
# Load the Core path helpers
config.include Blast::Core::Engine.routes.url_helpers
end
.rspec
fileThe below options to the .rspec
file are optional. These will give us a prettier output when we run our specs.
core/.rspec
--color
--require spec_helper
--format documentation
We can finally try to run the tests! From the Core
engine folder, run rspec
and you should get Figure 1:
Great, it’s working! Now let’s add some factories and some tests.
User
factoryCreate the folders core/spec/factories/
and core/spec/factories/blast/
. You can run the following command from inside the engine:
mkdir -p spec/factories/blast
Then add a file named user.rb
under spec/factories/blast/
(you can use the command below) and put the contents of Listing 4 in it:
touch spec/factories/blast/user.rb
core/spec/factories/blast/user.rb
module Blast
FactoryBot.define do
factory :user, class: 'Blast/User' do
email { Faker::Internet.email }
password { 'password' }
password_confirmation { 'password' }
end
factory :admin, class: 'Blast/User' do
email { Faker::Internet.email }
password { 'password' }
password_confirmation { 'password' }
admin { true }
end
end
end
It’s a pretty simple factory using the DSL from factory_bot
. Note the use of class: 'Blast/User'
to link to our User
model.
In Listing 4 you will notice that the User class has been assigned as
class: 'Blast/User'
A cause for many a headache are classes you might create with more than one word, for example ContactType
. If you assign the class as
class: 'Blast/ContactType'
you will spend countless hours trying to work out why the factory is not recognising your class. For this to work you need to assign the class as follows:
class: 'Blast/Contact_Type'
Of course, you can always assign the class the good old-fashioned way, and it will also work:
class: Blast::ContactType
User
modelWith our new factory, we can now write some tests. Create the folders core/spec/models/blast/
and create a file named user_spec.rb
inside. You can use the following command from the engine:
mkdir -p spec/models/blast && touch spec/models/blast/user_spec.rb
There is nothing overly complex in the User
model right now, so we’re going to add some pretty basic tests:
core/spec/models/blast/user_spec.rb
require 'rails_helper'
module Blast
describe User do
it 'has a valid factory' do
expect(FactoryBot.build(:user)).to be_valid
end
it 'is invalid without an email' do
expect(FactoryBot.build(:user, email: nil)).to_not be_valid
end
it 'is invalid without a password' do
expect(FactoryBot.build(:user, password: nil)).to_not be_valid
end
it 'is invalid with different password and password confirmation' do
expect(FactoryBot.build(:user, password: 'pass',
password_confirmation: 'pwd')).to_not be_valid
end
end
end
Run rspec
from the Core
engine folder and you should only get green lights:
Blast::User
has a valid factory
is invalid without an email
is invalid without a password
is invalid with different password and password confirmation
Finished in 0.10883 seconds (files took 0.57473 seconds to load)
4 examples, 0 failures
We did the hardest part: setting things up and writing the first tests. From now on, it shouldn’t be too hard for you to add more model tests.
Before we add the tests for the dashboard controller, we need to add two support files that will simplify our life.
The first file is the configuration for Devise. Create the file core/spec/support/devise.rb
with:
mkdir -p spec/support && touch spec/support/devise.rb
And paste the following inside:
core/spec/support/devise.rb
require 'devise'
RSpec.configure do |config|
config.include Devise::Test::ControllerHelpers, type: :controller
config.extend ControllerMacros, type: :controller
config.infer_spec_type_from_file_location!
end
This is the default configuration offered by Devise on GitHub.
The second file we need will add a simple way to log in users and admins in our tests. We also need to define which routes will be used.
Create the file core/spec/support/controller_macros.rb
with:
touch spec/support/controller_macros.rb
And put the following in it:
core/spec/support/controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
@request.env['devise.mapping'] = Devise.mappings[:admin]
sign_in FactoryBot.create(:admin)
end
end
def login_user
before(:each) do
@request.env['devise.mapping'] = Devise.mappings[:user]
user = FactoryBot.create(:user)
sign_in user
end
end
def set_engine_routes
before(:each) do
@routes = Blast::Core::Engine.routes
end
end
end
Now we can use login_admin()
and login_user()
in our controller specs and get back a logged in user. set_engine_routes()
will configure the routes to be used in the tests, preventing routing errors.
Finally, let’s add some specs for DashboardController
. Create the folder core/spec/controllers/blast/
and add a file named dashboard_controller_spec.rb
with the following command:
mkdir -p spec/controllers/blast && \
touch spec/controllers/blast/dashboard_controller_spec.rb
Here are the dashboard tests:
core/spec/controllers/blast/dashboard_controller_spec.rb
require 'rails_helper'
module Blast
describe DashboardController do
set_engine_routes
context 'signed out' do
describe 'GET Index' do
it 'does not have a current_user' do
expect(subject.current_user).to be_nil
end
it 'redirects the user to login page' do
get :index
expect(subject).to redirect_to new_user_session_path
end
end
end
context 'user' do
login_user
describe 'GET Index' do
it 'has a current_user' do
expect(subject.current_user).to_not be_nil
end
it 'should get :index' do
get :index
expect(response).to be_successful
end
end
end
context 'admin' do
login_admin
it 'has a current_user' do
expect(subject.current_user).to_not be_nil
end
it 'has a current_user who is an admin' do
expect(subject.current_user.admin).to be true
end
it 'should get :index' do
get :index
expect(response).to be_successful
end
end
end
end
The tests should be easy to read through, and self-explanatory. You can run them with rspec
from the Core
engine folder.
Some of you might notice that at this point your test crashes with the following error:
Failure/Error: config.extend ControllerMacros, type: :controller
NameError:
uninitialized constant ControllerMacros
This is because in some Linux machines, this line that we added in the rails_helper.rb
file earlier
Dir[File.join(ENGINE_RAILS_ROOT, 'spec/support/**/*.rb')].each { |f| require f }
requires the devise.rb
file in the /support
directory before the controller_macros.rb
file. To fix this, all you need to do is update the file to include the require
for controller_macros.rb
first, as you can see below:
require File.join(ENGINE_RAILS_ROOT, 'core/spec/support/controller_macros.rb')
Dir[File.join(ENGINE_RAILS_ROOT, 'core/spec/support/**/*.rb')].each do |f|
require f unless f.include?("controller_macros.rb")
end
You should also take note that we excluded the controller_macros.rb
file from the generic include block, in order to ensure it is not required twice.
Once you’ve run your tests, you should see the following output:
Blast::DashboardController
signed out
GET Index
does not have a current_user
redirects the user to login page
user
GET Index
has a current_user
should get :index
admin
has a current_user
has a current_user who is an admin
should get :index
Blast::User
has a valid factory
is invalid without an email
is invalid without a password
is invalid with different password and password confirmation
Finished in 0.14373 seconds (files took 0.58673 seconds to load)
11 examples, 0 failures
It’s now time to push our changes to GitHub. The flow is the same as in the previous chapter.
git status
git add .
git commit -m "Test Setup"
git push origin Chapter-6
In this chapter, we’ve added all the gems we needed to have a proper testing environment. We then configured them before writing our first set of tests.
rails_helper.rb
file.
In the next chapter, we’ll work on adding an admin panel that will allow admin users to manage the whole CRM.