Technology

Rails API Testing or "You've been doing it wrong"

For the last several years I've been developing Ruby on Rails applications.  Nothing fancy, and most of them just failed projects that didn't go anywhere.  During all this time I've trolled documentation on testing the app, saying "That would be nice to do one day", but I've finally had enough and I want to change my evil ways. I'm going to start writing test cases with my applications. To start with, I'm testing my API.  I didn't even know where to start.  I watched a Railscasts episode where the legendary Ryan Bates talks about how he tests.  There were some great comments in there about all these additional plugins.  It seemed pretty overwhelming to me at first and there was quite a learning curve.  So I wanted to write some of this down in case there was someone out there like me who was going to start down the path of correcting the error of there ways before 2015 hits.

Basic Rails Testing

The first thing to check was the guide.  Rails has great documentation! I ran:
rake test
Just to see if I would get any output.  After all, Ruby generates this for you automatically right?  Low and behold I got a failure! Well, first I was missing the test environment, so I put that in the config/database.yml.  Easy problem to solve! Next, my welcome controller gave me errors:
 1) Error:
WelcomeControllerTest#test_should_get_index:
ActiveRecord::StatementInvalid: SQLite3::ConstraintException: UNIQUE constraint failed: users.email: INSERT INTO "users" ("created_at", "updated_at", "id") VALUES ('2014-12-09 22:38:38', '2014-12-09 22:38:38', 298486374)
Ok, I already like this.  Looks like I need to intiate my test database? Turns out that Devise is the culprit.  A quick trip to StackOverflow solves the problem.  I fixed it like they said. Then I ran into another one with Devise that I added to my test/test_helper.rb file.   Its so nice to go over roads that other people have traveled! Running my test:
$ rake test
Run options: --seed 1932

# Running tests:

.

Finished tests in 3.422119s, 0.2922 tests/s, 0.2922 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
And I finally got tests to work!  Yay!  It looks like I'm doing it right. From here, I looked to test other API calls, but all the documentation said that I should probably start looking at rspec.  Apparently, that's how the cool kids are doing it.  (Or were doing it at some point when they wrote how to do it).  So after running rake test, that was the last testing I did with the distributed rails testing.

RSpec

This is the latest hotness in testing that I could find in my research.  Pretty much everybody seems to be using it.  I edited my Gemfile and added rspec-rails.  I also finally started grouping things so I wouldn't install these unnecessary gems on my production servers.  Spoiler alert:  My completed Gemfile looks like the below:
group :test, :development do
  gem 'capistrano', group: :development
  gem 'capistrano-rails', group: :development
  gem 'capistrano-bundler', group: :development
  gem 'capistrano-rvm', group: :development
  gem 'rspec-rails'
  gem 'factory_girl_rails'
  # for some quick tests
  gem 'shoulda-matchers', require: false
  # for fake names 
  gem 'ffaker'
end
As you can see, I added a few more gems after rspec-rails, but I'll get into those in a second. After doing bundle install I ran:
rails generate rspec:install
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
Now to test we run bundle exec rspec Ok, no tests to do yet!  Now to get to work!

Factory Girl & FFaker and other setup

The next step was to put Factory Girl.  Once again, Ryan Bates explains why Factory Girl is preferred over Fixtures.  I went back and added that to my Gemfile along with ffaker because I saw some cool things in that gem.  (The one thing not cool about ffaker was the documentation, but the code was easy enough to read. Next, I modified config/application as specified in this blog entry.
require File.expand_path('../boot', __FILE__)

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env)

ENV.update YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
module MyApp
  class Application < Rails::Application
    config.i18n.enforce_available_locales = true
    config.generators do |g| 
      g.test_framework :rspec,
        :fixtures => true,
        :view_specs => false,
        :helper_specs => false,
        :routing_specs => false,
        :controller_specs => true,
        :request_specs => true
      g.fixture_replacement :factory_girl, :dir => "spec/factories" 
    end 
  end 

end
I also had to add these modules into the rest of the environment.  I changed the spec/rails_helper.rb to have the below.  Everything else stayed the same:
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
require 'shoulda-matchers'
require 'ffaker'
Then I added the directory: mkdir spec/support I added the file spec/support/devise.rb
RSpec.configure do |config|
  config.include Devise::TestHelpers, :type => :controller
end
as well as the file spec/support/factory_girl.rb
RSpec.configure do |config|
  config.include FactoryGirl::Syntax::Methods
end
That has all my extra libraries used for my tests. Lastly, I setup the test database

rake db:test:prepare

A basic Tests

Now to set up some tests.  I thought it best to start off simple with a static page:

rails g rspec:controller welcome

This is the root of the homepage.  Following the documentation, I added some simple tests for the welcome page:

# this is spec/controller/welcome_controller_spec.rb
require 'rails_helper'

RSpec.describe WelcomeController, :type => :controller do
  describe "GET #index" do  
    it "responds successfully with an HTTP 200 status code" do
      get :index
      expect(response).to be_success
      expect(response).to have_http_status(200)
    end 

    it "renders the index template" do  
      get :index
      expect(response).to render_template("index")
    end 

  end 
end
I ran bundle exec rspec  and it worked.  (Though not at first, as I had to figure out how to configure everything like I set up above. )

Testing User Model

rails generate rspec:model user Since I already have a user model.  The list of spec modules to add are listed here. Since we have a model we are testing, we need to generate the fixture for it.  Here's how I made it work with my Devise implementation: spec/factories/user.rb
FactoryGirl.define do
  factory :user , :class => User do  
    username { Faker::Internet.user_name }
    password "foobar123"
    password_confirmation { |u| u.password }
    email { Faker::Internet.email }
  end 
end
The part that was most important that stumped me for a while was not putting the { } around Faker::Internet.email.  Since my tests tests for unique emails, it kept failing.  Putting the {} around Faker::Internet.email made sure it was unique on each call. There's a lot of documentation on cleaning up the database by using the database_cleaner gem.  I'm not using it right now.  ffaker generates all kinds of new things for me, so I don't worry about it.  I suppose that the database would need to be initialized though from time to time. This could be accomplished with:
RAILS_ENV=test rake db:drop db:create
rake db:migrate RAILS_ENV=test
Next I added the user model test spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, :type => :model  do  

  before {
    @user = FactoryGirl.build(:user)
  }

  subject { @user }

  it { should be_valid }
  it { should respond_to(:email) }
  it { should respond_to(:password) }
  it { should respond_to(:password_confirmation) }

  it { should validate_presence_of(:email) }
  it { should validate_uniqueness_of(:email) }
  it { should validate_confirmation_of(:password) }
  it { should allow_value('foo@domain.com').for(:email) }

  it { should respond_to(:authentication_token) }
  it { should validate_uniqueness_of(:authentication_token) }

end
This uses the shoulda-matches gem quite heavily and seems to be a good start to testing my user model.  Unit test check!
$ bundle exec rspec spec/models/user_spec.rb 
..........

Finished in 0.12633 seconds (files took 2.34 seconds to load)
10 examples, 0 failures

 Testing the API Controller

Next, I wanted to check the API for when users authenticate.  The way my API works (and the way I assume most work this way) is that the user will send a username (or email) and password and from that the application will send back an API token.  This makes subsequent calls stateless.  So I'll test my session login  controller: rails g rspec:controller api/v1/sessions

 I was happy to see it created spec/controllers/api/v1/sessions_controller_spec.rb  just like how I have my API!

Here's the first version of my working sessions_controller_spec.rb file:
require 'rails_helper'

RSpec.describe Api::V1::SessionsController, :type => :controller do

  describe "POST #create" do
    before(:each) do  
      # set this as recommended by Devise so tests pass. 
      @request.env["devise.mapping"] = Devise.mappings[:user]
      @user = FactoryGirl.create :user
    end 

    context "when the credentials are correct" do  
      before(:each) do
        credentials = { :user => {email: @user.email, password: @user.password } } 
        post :create, credentials
      end 
     
      it "returns response status 200 given credentials" do
        #puts @user
        #@user.reload
        expect(response.status).to eq(200)
      end 
     
      it "returns the user authentication token" do  
        result = JSON.parse(response.body)
        expect(result['auth_token']).to eq @user.authentication_token
      end 

      it "returns the username" do
        result = JSON.parse(response.body)
        expect(result['username']).to eq @user.username
      end 

      it "returns the email" do
        result = JSON.parse(response.body)
        expect(result['email']).to eq @user.email
      end 
    end 
  end 
end
Running this test:
$ bundle exec rspec spec/controllers/api/v1/sessions_controller_spec.rb 
....

Finished in 0.10525 seconds (files took 2.18 seconds to load)
4 examples, 0 failures
Wow!  I feel like a real hipster programmer now!  Testing!

More Tests

This is just the beginning.  I am now a convert and subscribe to the theory that you should spend about half of your time writing test cases.  It pays off in the long run.  I have more to go, but from now on with each line of code I write I'll be writing test cases.  It seems that I still have a lot of catching up to do with the current system.