Chapter 6

The Core Module: Automated Tests with RSpec

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

6.1. Setting up a testing environment

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.

Listing 0.1: Added Test 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.

6.2. Generating the test database

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'

6.3. Generating RSpec files

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

6.4. Updating the rails_helper.rb file

Since 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:

  • We need to require all the testing gems we need: factory_bot_rails, faker and database_cleaner.
  • We need to update the location of the support files.
  • We need to add some configuration for Database Cleaner.
  • Finally, we need to load the Core path helpers.
Listing 0.2: Updated rails_helper.rb 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

6.5. Adding some options to the .rspec file

The below options to the .rspec file are optional. These will give us a prettier output when we run our specs.

Listing 0.3: Options for .rspec file core/.rspec
--color
--require spec_helper
--format documentation

6.6. Running the tests

We can finally try to run the tests! From the Core engine folder, run rspec and you should get Figure 1:

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_06/rspec_no_examples
Figure 1

Great, it’s working! Now let’s add some factories and some tests.

6.7. Creating the User factory

Create 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
Listing 0.4: Contents of user.rb factory 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.

Box 6.1. Class names in factories

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

6.8. Adding tests for the User model

With 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:

Listing 0.5: User Spec 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.

6.9. Adding Dashboard 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:

Listing 0.6: Devise configuration for RSpec 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:

Listing 0.7: controller_macros.rb contents 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:

Listing 0.8: 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.

Box 6.2. Crashing test on some Linux machines

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

10 Pushing Our Changes

It’s now time to push our changes to GitHub. The flow is the same as in the previous chapter.

  1. Check the changes:
    git status
    
  2. Stage them:
    git add .
    
  3. Commit them:
    git commit -m "Test Setup"
    
  4. Push to your GitHub repo if you’ve configured it:
    git push origin Chapter-6
    

11 Wrap Up

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.

11.1 What did we learn?

  • How to fix Rails files to work properly with engines.
  • How to configure the testing gems in the rails_helper.rb file.
  • How to write tests for models and controllers inside an engine.

11.2 Next Step

In the next chapter, we’ll work on adding an admin panel that will allow admin users to manage the whole CRM.