Dec 30, 2015
Table of contents:
A big part of the Ruby philosophy is testing. I think it’s hard to argue against the value of having a suite of automated tests that can run against your codebase to catch regressions.
Test Driven Development is the process of writing tests to drive the implementation. By writing the test first you incrementally get to the solution. This means you end up with better tests that fully cover the functionality you just wrote without the biases you might inadvertently introduce if you wrote the test after the implementation.
A couple of weeks ago we looked at MiniTest. I prefer to use MiniTest because it’s really straightforward.
In today’s tutorial we will be looking at doing TDD for Active Record Models with MiniTest.
First we need to create a new Rails project. If you haven’t already got a Rails project to work with take a look at Getting started with Ruby on Rails.
Next we need to create the model class, migration and test file:
bin/rails g model Article title:string slug:string published_at:datetime
I’m keeping this fairly simple for now, a blog article isn’t much use without a body, but we can cross that bridge another day.
You will also notice that I’m generating the model class before writing the failing test. I think it’s fine to allow Rails to generate what we need before writing the first test.
Rails will automatically generate the files we are going to need including the model class, a test class, and a database migration. I’m going to make a quick adjustment to the migration file:
class CreateArticles < ActiveRecord::Migration
def change
create_table :articles do |t|
t.string :title, null: false, index: true
t.string :slug, null: false, index: true
t.datetime :published_at
t.timestamps null: false
end
end
end
Here I’m adding a couple of options to the title
and slug
columns to ensure at a database level that they are required and unique. You don’t have to do this as we’ll be enforcing this at the application level.
Now you can set up the database by running the following command in terminal:
bin/rake db:setup
When we used the Rails generator to create the model class, Rails also created a matching test class. You can find this class under the test/models
directory called article_test.rb
:
require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
end
To make writing our tests easier I’m going to add a setup
method that will automatically create a new Article
instance before each test:
def setup
@article = Article.new
end
The first test I will write will be to ensure that the title
is required. We have specified this is the case at the database level, but the model is currently not enforcing this rule:
def test_title_is_required
@article.valid?
assert_includes(@article.errors[:title], "can't be blank")
end
In this test I’m checking to see if the @article
is valid, and then asserting that the title
errors has the specific error I’m looking for.
If you run this test now you will see it fail. With a failing test in place we can now write the code to make it work!
In the article.rb
model class, add the following validation definition:
class Article < ActiveRecord::Base
validates :title, presence: true
end
Now if you run that test again it should pass.
We now need to ensure that the slug
field is also required. We could do this by duplicating the test method, and I’m not totally against that, but there is a better way to test this type of functionality.
Instead of writing out these boilerplate tests, we can use a gem called Shoulda to make this a lot easier.
To install Shoulda, open your Gemfile
and add the following:
group :test do
gem 'shoulda'
end
Now run the following command from terminal to install the new gem:
bundle install
Now we can simply write the presence tests like this:
class ArticleTest < ActiveSupport::TestCase
should validate_presence_of(:title)
should validate_presence_of(:slug)
end
If you run your tests again you should see the second test fail. To fix this we can add the same validation rule for the slug
attribute to the Article
class:
class Article < ActiveRecord::Base
validates :title, presence: true
validates :slug, presence: true
end
Shoulda also has an assertion helper for asserting for uniqueness. However, because we have required fields on this model, it’s a bit of a pain in the arse to make it work.
Shoulda is supposed to make our lives easier, not harder, so in this case we can just write out the method:
def test_title_and_slug_should_be_unique
@article.title = 'Hello World'
@article.slug = 'hello-world'
@article.valid?
assert_includes(@article.errors[:title], 'has already been taken')
assert_includes(@article.errors[:slug], 'has already been taken')
end
If you run this test you should see it fail!
The first thing I’m going to do is to open up the articles.yml
file under the fixtures
directory. The Rails generator also generated this file when we ran the command earlier.
I’m just going to delete the contents of the file and replace it with:
hello-world:
title: "Hello World"
slug: "hello-world"
This will setup the article and make it available for each test.
Next I can add the following definition to both the title
and slug
attributes of the model:
uniqueness: true
If you run the tests again now, you should see them pass.
Next we need to ensure that the format of the slug
is correct. Here is the test:
def test_slug_should_be_correct_format
@article.slug = 'All Of Your Base'
@article.valid?
assert_includes(@article.errors[:slug], 'is not a valid slug')
@article.slug = 'All-Of-Your-Base'
@article.valid?
assert_includes(@article.errors[:slug], 'is not a valid slug')
@article.slug = 'all-of-your-base'
@article.valid?
assert_empty(@article.errors[:slug])
end
Some testing purists will say you should only have one assertion per test, but I think this is fine to combine it into one test. If you run this test, you should see it fail.
To make this test pass we can add the following validation to the slug
property on the Article
class:
format: { with: /\A[a-z0-9]+(?:-[a-z0-9]+)*\Z/ }
Now if you run the tests again you should see them pass.
Ensuring that slugs are the correct format is something that I’m going to want to do across multiple models in this application. So, whilst this is a premature abstraction, I can create a custom validator to do that.
First create a new directory under app
called validators
. Rails will automatically pick up this new directory.
Next create a new file under the validators
directory called slug_validator.rb
:
class SlugValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A[a-z0-9]+(?:-[a-z0-9]+)*\Z/
record.errors[attribute] << (options[:message] || 'is not a valid slug')
end
end
end
Now you can replace the initial format validation with:
slug: true
If you run your tests again you should see that they still pass.
Finally I want to be able to check to see if a model object has been published. Here is the test:
def test_is_published
assert_not(@article.published?)
@article.published_at = DateTime.now
assert(@article.published?)
end
First I check to make sure the article is not published. Next I set the published_at
attribute. Finally I assert that the article is published.
Once again, if you run this test you should see it fail. To make the test pass we can add the following method to the Article
class:
def published?
!published_at.nil?
end
Now if you run the tests again you should see them all pass!
TDD isn’t very difficult, it’s just a process you need to get your head around.
It can be tempting to just skip doing TDD when you are pushed for time.
But I often find that it’s actually a lot quicker to just do TDD because you get your code working with less problems.
In today’s tutorial we’ve done some basic TDD to flesh out the business logic of the Article model.
In the coming weeks we will be using TDD to test some more advanced scenarios and functionality.