Saturday, January 31, 2015

Creating a Rails Model with a Non-default Primary Key

One of the main Rails philosophies is "convention over configuration". This works great most of the time, but sometimes it's best to do something outside of convention. One example is the primary key for models. Sometimes you want the key to be something other than ID; maybe you want it to be the ID of a related model.

This is possible to set up in a migration, you just need to do a few important things:
  • Set id: false on the create_table line
  • Set your new primary key to null: false
  • Add a unique index on your new primary key
Here is an example:

 class CreateBearHabitats < ActiveRecord::Migration  
   def change  
     create_table :bear_habitats, id: false do |t|  // prevents a default ID column from being created
       t.belongs_to :bear, null: false //creates a bear_id column that can not be null
       t.string :habitat  

     add_index :bear_habitats, :bear_id, unique: true // ensures that the bear_id column is unique
This BearHabitat model will now use bear_id as a primary key instead of the standard id.

Monday, January 26, 2015

Testing Controllers in Rails Engines

Testing your controllers in a Rails engine requires just a few extra steps compared to testing in a Rails application.

For example, you're working on an engine called Animals, and you have a BearsController with an index action that returns all the bears you've registered.

With a controller like this:
 // app/controllers/animals/bears_controller.rb  
 module Animals  
   class BearsController < ApplicationController  
     def index  
       // return all bears  
and a routes file like this:
 // config/routes.rb  
 Animals::Engine.routes.draw do  
   get '/index' => 'bears#index'  
you will need to update your dummy routes file to look like this:
 // spec/dummy/config/routes.rb  
 Rails.application.routes.draw do  
   mount Animals::Engine => "/animals"  
and in your controller spec you will need to add one extra line compared to normal:
 // spec/controllers/animals/bears_controller_spec.rb  
 RSpec.describe BearsController do  
   // this is the key line!  
   routes { Animals::Engine.routes }
   describe 'GET :index' do  
     it 'does the thing' do  
       get :index
       // test that all bears are returned  
and now your controller test will go to the appropriate routes :)

Monday, January 19, 2015

Testing Loaded Devise Modules In Your Models

So you're using Devise or devise_token_auth for user authentication in your Rails project. Awesome! Both are great libraries for authentication. Now, how are you testing your code? You probably have feature tests to ensure tasks like creating a new user work (and if you don't, stop reading this right now and make them!). You might even have controller tests to ensure, for example, that each action authenticates the user before proceeding.

What about your models? They are important too, and fortunately these libraries handle most of the model work in the background for you. There are still things you can test though! The main thing that come to mind are ensuring that you only load the devise modules that you want. For devise_token_auth, you also want to ensure you load the model concern.

Devise Modules

Let's say that you're only using the Database Authenticateable and Registerable modules. With Devise, your model might look something like this:

 class User < ActiveRecord::Base  
    devise :database_authenticatable, :registerable  

Devise provides a method called devise_modules that returns an array of symbols representing the modules that you include. With this, you can easily test that you're using only these two modules by doing this:

 // spec/models/user_spec.rb  
 // using RSpec and FactoryGirl for testing  
 RSpec.describe User do  
    it 'has the right devise modules loaded' do  
       user =  
       expect(user.devise_modules).to eq([:database_authenticatable, :registerable])  

What if there was a business requirement that Omniauth was not available? Well, you can test that easily too:

 it 'does not load the Omniauth module' do  
   user =  
   expect(user.devise_modules.include?(:omniauthable)).to be_falsey  

DeviseTokenAuth concern

With devise_token_auth, you also include a concern to add extra methods to User that are used internally. How can you test this? Easily!

 it 'loads the DeviseTokenAuth concern' do  
   expect(User.ancestors.include?(DeviseTokenAuth::Concerns::User)).to be_truthy  


With these tests, you can be sure that your model includes everything from Devise that you're expecting it too. This is great insurance for when you decide to refactor your model 6 months from now. It makes your assumptions clear, ensures that any changes are explicit, and allows you to test your user model is using Devise without having to run your expensive feature specs.

Friday, January 16, 2015

Rake Database Tasks Outside Rails

My first content post is, ironically, not directly about Rails. It is about using a very common Rails feature - rake database tasks - in a Ruby project that utilizes ActiveRecord but not Rails.

By default, tasks like rake db:migrate are available in Rails but not in a Ruby project that includes ActiveRecord. Fortunately, with a couple of lines added to your Rakefile this issue can be fixed.
 // Rakefile
 DatabaseTasks.env = ENV['YOUR_ENV_VAR'] || 'development'  
 DatabaseTasks.db_dir = 'db'  
 DatabaseTasks.database_configuration = YAML.load_file('config/database.yml')  
 DatabaseTasks.migrations_paths = "#{DatabaseTasks.db_dir}/migrate"  
 task :environment do  
  ActiveRecord::Base.configurations = DatabaseTasks.database_configuration  
  ActiveRecord::Base.establish_connection DatabaseTasks.env.to_sym  
 load 'active_record/railties/databases.rake'  
And voila. Now, you can use create, migrate, and all of the other rake db tasks you're used to in a Rails project.

Sunday, January 4, 2015

Hello World!

As I have been learning Ruby on Rails the past several months, I have searched the Internet a multitude of times when trying to solve an issue. Sometimes I have found the solution (thanks Stack Overflow), but other times I have had to figure things out on my own, especially when dealing with RSpec. I will be posting the problems I encounter here and how I solved them. I hope you find them useful!