cult3

TDD Active Record Models with MiniTest

Dec 30, 2015

Table of contents:

  1. Generate what we need
  2. Writing the first test
  3. Validating for uniqueness
  4. Validating the format of a field
  5. Creating custom methods
  6. Conclusion

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.

Generate what we need

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

Writing the first test

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

Validating for uniqueness

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.

Validating the format of a field

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.

Creating custom methods

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!

Conclusion

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.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.