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 create
method.
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 rspec
cli 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.