Pothibo

How did I start testing my rails project?

Tests can save your ass, I believe this much is true. On the other hand, getting started with tests can be very difficult. When I started, here's the kind of questions that made my life really painful:

  • Should I test this? or that?
  • What should I trust?
  • My functions do 3 different things. What should I test?
  • Should I start testing the views? the models? the controllers?
  • I only want to test this small piece, why is it so complicated to generate that object?

Eventually, I found my way. And now that I've been using tests for a little while, I believe it is time to give back to the community. If you haven't quite found the way to start testing your projects, this post is for you.

Being new at testing, you should focus on small, concise tests. Unit tests are a great way to get started. They are the simplest tests to setup and if you test your models properly, it will increase your confidence in your application. Your controller, views and helpers are built on top of your models. If your models are well tested, the other parts of your system will be inherently more robust.

Start with unit tests

This post will talk exclusively about unit tests. When starting with unit tests, there is different things you might want to test

  • states;
  • scopes;
  • validations.
  • callbacks;
  • relationships between objects;

This can become quite overwhelming at first. When starting, focus on small tasks. It does not matter if your tests are too simple, you are starting. Cut yourself some slack!

States

Testing state is the process of testing attributes on a single model. An example could be an onboarding process: The user entered his e-mail & password and every time the user logs in, you ask him to fill up one more field until all the informations are gathered.

class User < ActiveRecord::Base

def missing_information?

missing_informations.any?

end

def missing_informations

%w(name birthdate gender).delete_if do |attr|

!send(attr).nil?

end

end

This is the kind of method you need to look for when starting. They are self contained and the return value is easy to expect. It's possible that your project has none of them. Anyhow, here's how you could start testing with the code above.

test/unit/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

def setup

@user = User.allocate

end

test "missing informations" do

assert @user.missing_informations?, "A new user should be missing information"

count = @user.missing_informations.count

@user.name = "Joé Juneau"

@assert_equal count - 1, @user.missing_informations, "Naming the user should reduce the number of missing fields"

end

end

There's a few things I want to talk about here that might not be obvious for people who haven't looked at test before. The setup method is called by default on every test. That means that you can't have two tests sharing a state, i.e @user is reset every time. This is the reason why I use many assertions in the same test. Also, notice my use of User.allocate. This let you use ActiveRecord objects without the underlying database connection.

You can see that I test behavior, not implementation details. Testing the implementation would have been something like this

test "missing informations" do

assert @user.missing_informations?, "A new user should be missing information"

assert_equal @user.missing_informations, %w(name birthdate gender), "Missing informations do not match"

@user.name = "Joé Juneau"

assert_equal @user.missing_informations, %w(birthdate gender), "Missing informations do not match"

end

The latter will be much harder to maintain. If you decide to add a field in the future, you will have to change your model to add that field, and then you will have to modify your tests so the array matches. That's unnecessary. Tests evolve with time and if you write them in a restrictive way, you will always have to come back to them because they will fail every time you change something.

Always test on expected behavior.

Scopes

Testing scopes is to make sure that every scopes are playing nicely with each other. From this point on, you will need to have a test database running. This is usually working out of the box in rails (using SQlite) and the setup is pretty straightforward. You can read more about setting up your database reading through the rails' guide.

Again, the goal here is to test the behavior. Let's presume we have a Recipe model that can be created by a user.

class Recipe < ActiveRecord::Base

belongs_to :user

scope :created_after, -> (date) { where("recipes.created_at > ?", date }

scope :author, -> (user) { where("recipes.user_id = ?", user.id }

scope :published, -> (is_published) { where("recipes.published = ?", is_published }

end

You could do something like this

test/unit/recipe_test.rb

require 'test_helper'

class RecipeTest < ActiveSupport::TestCase

def setup

@user = User.create()

@recipe = Recipe.new

@recipe.author = @user

@recipe.created_at = 6.months.ago

@recipe.save

end

# Test stuff...

end

Again, it would be very hard to maintain and you would have a lot of boilerplate code for all your different scopes. Fixtures is what you need to get you started. Fixtures can be daunting at first, but really helps you down the road. Fixtures support your tests suite. That means you write a new fixture when you need a record in a certain state for a test you are writing. Don't try to write different bunch of fixtures right away, you won't know what to write! (I say this because it happened to me. True story)

So back to the tests. The requisites for the tests to pass are as follow:

  • Need a user so I can fetch Recipe by user;
  • Need to have at least one recipe with published state on;
  • Need to have a timestamp on the Recipe object.

With this list in mind, it's easy to create the necessary features.

test/fixtures/user.yml

Lewis:

name: "Lewis Ricky Jr."

test/fixtures/recipe.yml

hotdogs:

published: true

created_at: <%= 6.months.ago %>

user: Lewis

Notice three things:

  1. You can create relationships in YAML like you would in ruby.
  2. You can use ruby in the YAML file. It can evaluate ruby but can't evaluate stuff like <%= User.first %>.
  3. YAML is not validated. You can enter any data you want. If you do have validation in your models, make sure your data reflect that.

Now, all the fixtures needed for the tests to pass have been created, time to test the scopes!

test/unit/recipe_test.rb

require 'test_helper'

class RecipeTest < ActiveSupport::TestCase

test "can query recipes"

@date = 1.year.ago

@user = users(:Lewis)

@recipes = Recipe.author(@user).created_after(@date).published(true)

assert @recipes.count > 0, "There should be at least one recipe"

@recipes.each do |recipe|

assert recipe.user.eql?(@user), "User should match #{@user.name}"

assert recipe.published, "Recipe should be published"

assert recipe.created_at > @date, "Recipe should be created after #{@date}"

end

end

end

Your fixtures will grow over time and there's a real chance that your scope will not return the same list as the time goes on. So instead of asserting against a number of result, another approach is to make sure you have at least one result and that every record match the expected behavior.

The test above might be loose meaning that it's not covering some cases that you may want to cover. If that's the case, I suggest you create other tests for corner cases with a description that really explains the behavior you are trying to cover.

Validations

Testing validations is a cat-and-mouse type of thing. Start small, start with some variations that you think might fails and test those. When a bug occurs with a validation, add the scenario in your test and make it pass.

In a way, validations and their tests get better over time.

For this example, the recipe is created in two steps. First, the user enters a title and submit that title. If the title does not exist, the recipe is created and the user can add a description and ingredients. For this to work, the recipe must validate the ingredients, not on creation, but on updates. As for the title, it should be validated from the creation to every subsequent updates.

class Recipe < ActiveRecord::Base

validates_uniqueness_of :title

validates_presence_of :title

validates_size_of :ingredients, on: :update

end

When testing validations, the goal is to check if the validations pass. The goal is not to save to the database. This is important because there's sometimes a feeling that testing validations should be done the whole way through e.g. saving the object to the database. The problem with this approach is that if you have callbacks, i.e sending an e-mail to a newly created user, callbacks will fail and an exception might be raised in your unit tests.

So, instead of saving to the database, it's better to simply call the valid? method on the object. When called, it will first go through all the validations and return the result. Here's how I test my validation.

test/unit/recipe_test.rb

class RecipeTest < ActiveSupport::TestCase

test "validation for the title" do

@recipe = Recipe.new

assert !@recipe.valid?, "Shouldn't validate a recipe without a title"

assert @recipe.errors.keys.include?(:title), "There should be an error about the title"

@recipe.title = ""

assert !@recipe.valid?, "Shouldn't validate a recipe with an empty title"

assert @recipe.errors.keys.include?(:title), "There should be an error about the title"

@recipe.title = "A title"

@recipe.send("perform_validations")

assert !@recipe.errors.keys.include?(:title), "Title should be valid"

end

end

Test relationships

Don't test normal relationships. What I mean by that is that you should trust rails in many of your relationships. Taking the Recipe model again

class Recipe < ActiveRecord::Base

belongs_to :user

has_many :ingredients, dependent: :destroy

end

In this case, you shouldn't have to test anything. These are standard rules and there are tests inside rails that cover everything here. Whatever you would test would be a duplicate.

So what should you tests?

I test relations that I override. Here's an example of what I mean

class Recipe < ActiveRecord::Base

belongs_to :user

def user=(resource)

unless self.user_id.nil?

raise "Can't modify author"

end

super resource

end

end

Because I modified the normal behavior of the relationship, it now makes sense to test it.

test/unit/recipe_test.rb

class RecipeTest < ActiveSupport::TestCase

test "exception is raised if the recipe change user" do

@recipe = recipes(:hotdogs)

assert_raises StandardError do

@recipe.user = User.new

end

end

end

Callbacks

In my experience, I have tested only 2 different types of callbacks:

  • Mail delivery (user subscription)
  • Attribute modification

When testing the former, you don't want to test the delivery of the email. You just want to make sure that the email is queued. Again, this is testing the behavior. The process of sending the email is already tested by rails. There's no use of testing that.

class User < ActiveRecord::Base

after_create :send_confirmation_email

protected

def send_confirmation_email

#send your e-mail

end

end

test/unit/user_test.rb

class UserTest < ActiveSupport::TestCase

test "sending a confirmation email when a user is created" do

assert ActionMailer::Base.deliveries.empty?, "There shouldn't be email in the queue"

@user = User.create(email: "my@email.com")

assert !ActionMailer::Base.deliveries.empty?, "A confirmation email should be queued for delivery."

end

end

As for the latter, it's basically the same as the first step (testing states). The only addition is updating/deleting/creating the resource and making the assertions afterward.

class Recipe < ActiveRecord::Base

before_save :increase_update_count

protected

def increase_update_count

return unless self.persisted?

self.update_count = self.update_count + 1

end

end

test/unit/recipe_test.rb

class RecipeTest < ActiveSupport::TestCase

test "the number of update is tracked" do

@recipe = Recipe.new.save(validate: false)

assert_equal 0, @recipe.update_count, "Update count should not be increased when creating a recipe"

@recipe.title = "New recipe"

@recipe.save(validate: false)

assert_equal 1, @recipe.update_count, "Update count should be increased when updating a recipe"

end

end

Notice how I skip the validation for this test. This is on purpose. I don't need to validate because the test only focus on the update field. The validations were just extra code that wasn't needed on this particular test. This is subjective so you can create a validated recipe or not, it's up to you.

More to come later

I will write about the other types of tests in the next few posts. So, if you like this post, follow me on Twitter until then...

Stay tuned!

P.S. If you're wondering how to test helpers in rails, you might be interested in a post I did a few weeks back that showed how to test helpers in rails!

Get more ideas like this to your inbox

You will never receive spam, ever.