Building Rails Engines/Ruby gems — The TDD Way

Developing software with Ruby is fun because you can build something with less code. It is fun, until you encounter bugs or errors in your code. Adopting Test-Driven Development (TDD) can boost your team’s productivity, along with using some traditional debugging tools like byebug or pry.

At Bloom, I learned how to build Ruby gems and Rails Engines with TDD. Testing Ruby gems/Rails Engines can be hard at first. With RSpec for unit tests, and FactoryBot for generating Ruby objects as test data, you can practice the flow on how will you code from now on.

Creating Rails Engine/ Ruby gem

To create a gem, you must run:

$ bundle gem <gem-name>

or if it is a Rails Engine:

$ rails plugin new <engine-name> --mountable
  • Note that it is more recommended to use the --mountable flag when creating a Rails Engine

More info: https://guides.rubyonrails.org/engines.html#generating-an-engine

Setting up RSpec

Ruby gem
If you are creating a simple Ruby gem, you must choose RSpec as your unit test framework when you first create the gem, or you can do it manually. You must need RSpec for your unit tests. Include this line in your .gemspec file

s.add_development_dependency "rspec"

Rails Engine
If you are creating a Rails Engine, you need the rspec-rails gem:

s.add_development_dependency "rspec-rails"

then do $ bundle install command. If you are creating a Rails Engine after running bundle install, you will need to run:

$ rails generate rspec:install

to generate spec/ folder, inside it is spec_helper.rb and rails_helper.rb . Make sure your .rspec file looks like this:

--color
--require rails_helper

You must require rails_helper , and in rails_helper.rb, require spec_helper on top of the file:

# on top of rails_helper.rb
require 'spec_helper'
# rest of the code...

and change this line:

require File.expand_path('../../config/environment', __FILE__)

to this:

require File.expand_path('../dummy/config/environment.rb', __FILE__)

to make RSpec work in the dummy app it created.

Setting up FactoryBot

Ruby gem
Add this to your .gemspec file:

s.add_development_dependency "factory_bot"

then $ bundle install

Rails Engine
Add the rails keyword in factory_bot in .gemspec:

s.add_development_dependency "factory_bot_rails"

then $ bundle install. After that, create a support/ folder and inside it, create a factory_bot.rb file that contains:

require 'factory_bot_rails'
RSpec.configure do |config|    
config.include FactoryBot::Syntax::Methods
end

Now, back to rails_helper.rb , require factory_bot after requiring rspec/rails . Code looks like this:

require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../dummy/config/environment.rb', __FILE__)
# ...
# require factory_bot after this
require 'rspec/rails'
# ...
# factory_bot (here)
require 'support/factory_bot'

After that, inside our spec/ folder, create factories/ folder. Filename is test.rb (matching model filename). You can now define your factories like this:

FactoryBot.define do
factory :test_model, class: Test
end

Theclass hash is your model in app/models/ . More documentation of FactoryBot here.

This is for the user factory (spec/factories/user.rb):

FactoryBot.define do    
factory :user_model, class: User
end

Generate routes, models and controllers

Now let’s start doing some real work. Rails Engines are usually made for modularization. The aim of making a Rails Engine is to wrap a “mini” Rails application with a specific subset of functionality. In that way, it is easy to share it in different projects, or you could say “plug-and-play” like how normal gems work.

So to start, we must put some routes in our config/routes.rb file,

TestEngine::Engine.routes.draw do  
scope module: 'rails' do
namespace :api do
namespace :v1 do
post 'test', to: 'test#create'
end
end
end
end

We are expecting a create method in our test_controller.rb . Let’s create that:

$ rails g controller Api::V1::Test

Then we modify it:

module TestEngine
class Api::V1::TestController < ApplicationController
def create
value = test_params["test"]
head 400 and return if value != "success"

user = User.find_by(username: test_params["username"])
@test = Test.create(user: user, test: value)
render json: {
test: value
}
end

private

def test_params
params.permit(:test, :username).with_indifferent_access
end
end
end

Now, let’s generate the models:

For user model:

$ rails g model user

Then in user migrations:

class CreateTestEngineUsers < ActiveRecord::Migration[5.2]
def change
create_table :test_engine_users do |t|
t.string :username, :null => false
t.timestamps
end
end
end

For test model:

$ rails g model test

Then in test migrations:

class CreateTestEngineTests < ActiveRecord::Migration[5.2]
def change
create_table :test_engine_tests do |t|
t.string :test, :null => false
t.timestamps
end
end
end

An important part: Do not forget to mount the Rails Engine routes in your main Rails app (here, we put it in our dummy/config/routes.rb because we are just testing the functionality of our Engine).

Rails.application.routes.draw do
mount TestEngine::Engine => "/test_engine"
end

where the routes matching yourdomain.com/test_engine will be processed by the Rails Engine.

More detailed documentation of Rails Engines here.

Writing unit tests

It is time to test our Engine by unit testing. Our Engine is focused on providing RESTful API endpoints, so we will make a requests/api/v1/ folder under the spec/ folder. Inside the requests/api/v1/, we can name our spec as <controller-name>_spec.rb . We will name it as test_spec.rb because we are testing the test feature.

In our Rails Engine/ Ruby gem, we can write tests that integrate FactoryBot like this:

RSpec.describe "test" do
describe "POST /api/v1/test" do
context "Test successful" do
before do
# create an instance of the model
# from factory you created earlier
create(:user_model, username: "johndoe")
end
it "successful" do
post("/api/v1/test", {
params: {
test: "success",
username: "johndoe"
}
})
                test = 
JSON.parse(response.body)
.with_indifferent_access
expect(response).to be_successful
expect(test.test).to eq "success"
end
end

context "Test unsuccessful" do
it "unsuccessful" do
post("/api/v1/test", {
params: {
test: "unsuccessful",
username: "johndoe"
}
})
                test = 
JSON.parse(response.body)
.with_indifferent_access
expect(response).to_not be_successful
expect(response.code.to_i).to be 400
end
end
end
end

where you specify your hash :test_model from the factory you created on createmethod.

describe, context, it

The best practice on what will you put on the describe DSL is the HTTP verb you’re testing followed by the route on what you are hitting. context is just an alias method of describe . However, we are usingcontext to wrap a set of tests against one functionality under the same state. it keyword is used for describing what is the expected output on the test.

To summarize:

describe: to wrap a set of tests against one functionality

context: to wrap a set of tests against one functionality under the same state

it: tell the developer what is the expected output on the test

More of that here.

expect

expect is the method that basically tells the developer if the test passed or fail. We tell the expect on what value are we expecting, whether is it an integer or a string. More documentation of rspec-expectations here.

We use rspec-rails here, so here is the documentation for the rspec-rails

Running the tests

After writing the tests, run this command to execute it:

$ rspec spec/

The command will run all your specs under your spec/folder. To run a specific spec file, run:

$ rspec spec/test_spec.rb

Basically, rspec accepts file paths as its arguments. To run specific spec files, just separate it with white space:

$ rspec spec/test_spec.rb spec/another_test_spec.rb

rspec can also run specific tests in a spec file. To do this, you supply the line number that contains the describe, context, or it keyword:

# assuming there's context keyword in line 100 containing unit tests
$ rspec spec/test_spec.rb:100
# many spec files
$ rspec spec/test_spec.rb:100 spec/another_test_spec.rb:100

Lastly, you can also append multiple line numbers that contains the unit test block.

$ rspec spec/test_spec.rb:100:120:140

More detailed rspeccli commands here.

Summary

Building Rails Engines/ Ruby gems with RSpec and FactoryBot makes our testing easier. That means, more productivity and more efficiency in finding errors in the codebase. Testing is vital in Rails Engines/ Ruby gems because you require them in your app, and a single bug in those gems could blow up your app. Not only adopting TDD to your flow can eliminate the chances of errors, it can also make your app deployment safe and sound. Here in Bloom, we follow TDD rules, and I think that is good for scaling apps especially we test whether we are giving the correct HTTP responses or not.

Adopting TDD takes time, but I suggest you don’t take the blue pill

Sources: