How I test Rails applications

Patrik Bóna
Patrik on Rails
Published in
6 min readJul 1, 2017
Is everything checked? (pixabay.com)

When I was a Rails beginner I was amazed by Rails and Test Driven Development. But I often struggled with testing because I did not know what to test and how to test it. As a result I wrote too much tests and I tested wrong things.

In this article I am going to describe where I ended up after 4 years with Rails and what works for me the best.

Controller tests

These bring me the most value. Controllers are usual entry points to Rails applications and I want to be sure that each of them is working as expected.

Don’t be fooled by the name “Controller Tests”. They don’t test only controllers anymore.

What is nice about controller tests is that they became integration tests over time. I mean literally. This is true since Rails 5. When you generate a new controller, then its test file might look like this:

# test/controllers/users_controller_test.rbrequire 'test_helper'class UsersControllerTest < ActionDispatch::IntegrationTest
# ...
end

This is awesome, because now we can exercise the whole stack.

Controller tests can catch issues in controllers, views, models, view helpers and other code which is used during request processing.

As I said, controller tests bring me the most value. But what exactly do I test in them? Here are few examples.

I test that:

  • All records have been correctly created/updated.
  • Emails were sent.
  • Jobs have been scheduled.
  • An action has been executed. I test it directly by side effects of the action or I just stub some object if side effects are undesirable. Example of an action: Order refund.

I don’t test that:

  • Instance variable has been assigned.
  • A particular template has been rendered.

This is not a comprehensive list of things which I test in my controller tests, but it should give you an idea what to test.

Example:

# test/controllers/refunds_controller_test.rbclass RefundsControllerTest < ActionDispatch::IntegrationTest
test "refund order" do
order = orders(:for_john_doe)
PaymentGateway.any_instance.expects(:refund_charge)
assert_enqueued_emails 1 do
post refunds_url, params: { order_id: order.id }
end
order.reload
assert order.refunded?
assert_redirected_to order_path(order)
end
end

Controller tests are awesome because they exercise the whole stack. But this might be not enough when you depend on JavaScript which is executed on the client side. Enter system tests.

System tests

Systems tests are one level higher than controller tests. They test application from user’s point of view by simulating user actions via browser. This could be a headless browser (it doesn’t create a new browser window) or a real browser like Chrome, Firefox or Safari. Rails 5.1 uses Chrome by default.

Btw. it is kind of cool to watch your systems tests from time to time ;).

Unlike with controller tests I don’t tend to test all possible paths in system tests unless I am testing some critical component of our application.

A good example is our checkout (click on the “Test subscription” button). This is one of the most important parts of our application and it is rendered mostly on the client. Therefore we test a lot of different checkout scenarios with systems tests.

System tests are based on the capybara gem and they were added to Rails in Rails 5.1. Of course that we used them also before, we just did not call them system tests. They were usually called integration tests or feature tests. Also, before Rails 5.1 they required a special setup and installation of few gems. Fortunately this is not true anymore and you can use them straight after rails new.

If you want to see a nice system test, then check the following example from Basecamp:

Model tests

Model tests are useful, too. You just need to know what to test and what to NOT test.

I don’t test:

  • Rails validations. They are already tested in Rails.
  • Associations. Again, I don’t want to test Rails itself.
  • Simple scopes.

I test:

  • Custom validations. This can be also a regular expression in the format validation.
  • Complicated scopes. I want to be sure that I receive records which should be returned.
  • Custom methods and custom attribute writers.

Example:

# test/modes/user_test.rbclass UserTest < ActiveSupport::TestCase
test "downcase email address" do
member = User.new(email: "John.Doe@example.com")
assert_equal "john.doe@example.com", member.email
end
end

Helper tests

I love helpers and I prefer to use them instead of decorators and similar concepts. But I don’t test them too much. They are usually indirectly tested by controller tests.

However there are few helpers in our application which we test thoroughly.

For example we have a helper called subscription_description. It returns a subscription description based on current subscription state. There are 12 possible states and therefore 12 different descriptions. I want to be sure that we generate correct description for every state. Also, more importantly I want to be sure the helper doesn’t cause a HTTP 500 error because of a typo. Therefore I test it thoroughly.

Example:

class SubscriptionHelperTest < ActionView::TestCase
test "#subscription_description" do
subscription = subscriptions(:for_john_doe)
subscription.update!(expires_at: "2017-07-31", autorenew: true)
expected_description = "Renews on July 31st, 2017."
actual_description = subscription_description(subscription)
assert_equal expected_description, actual_description # other assertions
end
end

Object tests

From time to I need to extract logic from a controller (or somewhere else) to another object. I don’t do this often, because I don’t have issues with some logic in controllers, but when I do it I make sure that this object is thoroughly tested.

Rails doesn’t care where you put your objects. For example it can be app/models/, app/services/ or lib/some_integration/.

Let’s say I have a non-ActiveRecord model in called GiftCreator which lives in app/models/. Then its test might looks like this:

# test/models/gift_creator_test.rbrequire "test_helper"class GiftCreatorTest < ActiveSupport::TestCase
test "create a gift" do
GiftCreator.new.do_stuff
assert_stuff_is_done
end
end

Background job tests

I test background jobs in a similar fashion as regular objects.

Example:

# test/jobs/do_stuff_job_test.rbrequire "test_helper"class DoStuffJobTest < ActiveJob::TestCase
test "do some stuff" do
DoStuffJob.perform_now
assert_stuff_is_done
end
end

Mailer tests

Emails are critical part of our application therefore I want to be sure that our mailers don’t fail. I usually don’t test every detail of generated emails. It is enough for me to know that it works and it doesn’t fail.

Example:

# test/mailes/user_mailer_test.rbrequire 'test_helper'class UserMailerTest < ActionMailer::TestCase
test "welcome user" do
user = users(:john_doe)
email = UserMailer.welcome(user) assert_equal [user.email], email.to
end
end

Emails are generated and delivered asynchronously from a background job. But if they weren’t, I wouldn’t probably test mailers because they would be indirectly tested by controller (and other) tests.

Test Driven Development (TDD)

TDD means that your design is driven by tests. It works like this:

  1. You write a failing test.
  2. You write a code to make the test pass.
  3. You refactor the code.

It sounded great when I was learning Rails. But, nah. I don’t use TDD too much anymore. I don’t know what I am going to do, so how can I test it first? I still write some tests before the actual code, but usually I write my tests later. And I’m not alone.

Testing tools

I learned Rails together with RSpec, FactoryGirl and bunch of other tools like Timecop, spork, and I can’t remember what else.

I don’t use any of these anymore. I use default Rails stack instead. That is MiniTest and database fixtures. Also, since Rails 5.1 gave us system tests I don’t need to install a single additional tool or tune my test_helper.rb anymore.

Well, kind of. I still use tools like VCR, webmock or mocha when I need to, but these aren’t the tools which are needed in every Rails application.

That’s all. This is where I ended after 4 years with Rails. I am a single developer at Memberful. If something is not working as expected, then usually I am the one to blame. Thanks to automated testing this doesn’t happen too often.

What is your experience with testing? Do you test your applications? Do you struggle with testing? Let me know in comments, ping me on Twitter or subscribe to my newsletter and shoot me an email!

--

--