Skip to content

How to test your Rails models with RSpec

An introduction to TDD in Ruby on Rails; the red-green-refactor cycle and how to test a model's validations with RSpec.

6 min read
  • ruby
  • rails
  • testing
  • tdd

Originally published on Medium

In this post we'll talk about TDD and its advantages, walk through an example of doing TDD for models in Ruby on Rails using the famous RSpec gem, and finally I'll explain, from my point of view, why tests matter.

We've all heard about the famous "Test-Driven Development (TDD)", which is nothing more than: first write the tests and run them expecting them to fail (RED), write the code and run the tests again expecting them to pass (GREEN), and finally refactor the code.

The first time I heard about TDD I thought:

Write tests before the code? How do you test something you haven't even written yet? (ノ ಥ ウಥ )ノ

Then I understood why people looked at me like this whenever I forgot to write tests:

Do you even TDD, bro?

With all those doubts and more, I dove into the dark abyss of TDD, 'test-first', and so on [there are actually plenty of concepts and 'theories' around test-driven development; I won't explain them all because it would take us all day, night, month, year…]. Even though it took me a while to understand how it worked, I now thank God, Buddha, Allah, Zeus, Muhammad and every god there ever was ╰( ಠ ω ಠ )╯. With TDD I got to replace 'clicks' with 'code' — it blew my mind that you could test entire web interfaces purely through code.

I know TDD

Ok, enough talk — let's get to it!! (┛◉Д◉)┛

These days I program in Ruby and work with its famous framework, "Ruby on Rails".

RoR is an open-source web development framework optimized for programmer happiness and sustainable productivity.

Ruby on Rails also pushes you to avoid duplicating code (Don't repeat yourself) and lets you start working without wading through the painful configuration we so often have to deal with in our development environments (Convention over configuration).

Ruby on Rails

Ruby on Rails ships with testing support by default. When you create your application you'll notice it has a "test" folder, where you can put all the tests you want. You'll almost always have a 'models' folder for your model tests, another one called 'controllers' for controller tests, and so on. I say almost always because this structure depends on the type of application and on the developer [for example, with API's some developers prefer to create a folder called 'requests' and put the controller tests there, since they feel they aren't really testing the controllers but the requests].

# code taken from: http://guides.rubyonrails.org/testing.html
 
require 'test_helper'
 
class PostTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  test "the truth" do
    assert true
  end
end

The code above [finally, some code (´⌣ˋ)] is an example of the model tests RoR generates by default. But this time we'll use the RSpec gem instead.

# code taken from: https://github.com/rspec/rspec-rails
require "rails_helper"
 
RSpec.describe User, :type => :model do
  it "orders by last name" do
    lindeman = User.create!(first_name: "Andy", last_name: "Lindeman")
    chelimsky = User.create!(first_name: "David", last_name: "Chelimsky")
 
    expect(User.ordered_by_last_name).to eq([chelimsky, lindeman])
  end
end

RSpec uses a somewhat different syntax; you can check the documentation in the links I'll leave at the end of the post. While the default RoR syntax forces us to define tests directly (test "the truth" do…), RSpec lets us group them using 'describe', which describes — redundantly enough — what the group of tests will do [in RSpec a test is an 'example', and examples are defined with 'it'].

# source: https://github.com/rspec/rspec-rails
 
require "rails_helper"
 
RSpec.describe PostsController, :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
 
    it "loads all of the posts into @posts" do
      post1, post2 = Post.create!, Post.create!
      get :index
 
      expect(assigns(:posts)).to match_array([post1, post2])
    end
  end
end

In the example above you can see how much clearer and more readable the syntax is: we have one 'describe' grouping 3 examples (tests). By the way, if you noticed, both here (line 5) and in the previous example (line 4) we declare what's being tested with ":type => controller do" — it's not required, but it's good practice. Now let's look at a simple example of how to test models and their validations.

First, this is the structure of our model, which in this case is called contact.rb and has fields for firstname, lastname and email:

# ./app/models/contact.rb
 
class Contact < ActiveRecord::Base
  has_many :phones
  accepts_nested_attributes_for :phones
 
  validates :firstname, presence: true
  validates :lastname, presence: true
  validates :email, presence: true, uniqueness: true
 
end

These would be the tests for our 'contact' model using RSpec. Note that the file holding the tests follows the 'model_spec.rb' naming format:

# ./spec/models/contact_spec.rb
 
require 'rails_helper'
 
describe Contact do
  it "is valid with a firstname, lastname and email" do
    contact = Contact.new(
      firstname: 'Aaron',
      lastname: 'Sumner',
      email: 'tester@example.com')
    expect(contact).to be_valid
  end
 
  it "is invalid without a firstname" do
    contact = Contact.new(firstname: nil)
    contact.valid?
    expect(contact.errors[:firstname]).to include("can't be blank")
  end
 
  it "is invalid without a lastname" do
    contact = Contact.new(lastname: nil)
    contact.valid?
    expect(contact.errors[:lastname]).to include("can't be blank")
  end
 
  it "is invalid without an email address" do
    contact = Contact.new(email: nil)
    contact.valid?
    expect(contact.errors[:email]).to include("can't be blank")
  end
end

The first thing we do is a 'describe' to group every example (test) belonging to the model. Inside a 'describe' we can nest another 'describe' [we could use one to group the examples for a specific method of our Contact model, say a def name that returns a contact's full name]. We have 4 'it' blocks, or examples, all focused on testing the validations defined in contact.rb.

...
  it "is valid with a firstname, lastname and email" do
    contact = Contact.new(
      firstname: 'Aaron',
      lastname: 'Sumner',
      email: 'tester@example.com')
    expect(contact).to be_valid
  end
...

The first example (test) is 'is valid with a firstname, lastname, and email'. Our model validates each field with "presence: true". To test it, we first create a new Contact (lines 3–6) with whatever sample data we like [there's a gem called Faker that makes creating and using this kind of test data much easier].

Then we state what we expect to happen with 'expect' (line 7): we pass in our freshly created contact object, and with 'be_valid' we say this object should be valid. Valid because it satisfies presence: true ლ(╹◡╹ლ).

In the second test…

...
  it "is invalid without a firstname" do
    contact = Contact.new(firstname: nil)
    contact.valid?
    expect(contact.errors[:firstname]).to include("can't be blank")
  end
...

…we test that a contact must have a firstname, for obvious reasons. First (line 3) we create our contact object with a nil firstname, and what we expect (line 5) is that our contact object's errors include the message 'can't be blank'. We do the same for the remaining tests: one verifies the model is invalid without a lastname (it "is invalid without a lastname" do) and the other that it's invalid without an email (it "is invalid without an email" do). So in a way we've written the tests our Contact model needs. We did it!!

RSpec lets us write unit tests as well as integration tests.

I know TDD

To wrap up…

…at first it may feel hard, or even pointless, to write the tests before the code. But one of the biggest advantages is that by the end of the project you won't need a debugger, or to click around the UI like a crazy-tester. TDD also adds real value to your applications, because they come out with much higher quality. And not only that — it also helps you catch errors introduced when adding a new component, module or feature to the app. Some say TDD is dead, but that's not true; I think its focus is simply shifting.

TDD is dead…

And yes, I know, building an application with TDD may take you quite a bit longer [which is the main reason some developers don't write tests], but believe me, in the end you'll be glad you did: as I mentioned before, the software quality will be higher. I hope this post helps you and serves as a small introduction to TDD and the fabulous RSpec gem.

You can check out my other posts:

Links:

Thanks for reading! ( ゚,_ゝ゚) If you liked it, share it with your friends.

Thanks for reading