Home » Code » Writing Integration Tests in Ruby on Rails

Writing Integration Tests in Ruby on Rails

Posted by on July 6th, 2016

Writing Integration Tests in Ruby on Rails
In our exploration of Ruby on Rails we’ve covered testing quite a few times. It’s important to write tests at various layers of abstraction. A good test suite tests the appropriate details at each abstraction layer to provide good coverage of business rules and functionality whilst not being brittle or superfluous.

The final type of testing we haven’t covered yet are integration tests.

Integration tests test the application from the outside in, and usually involve multiple controllers that comprise a typical important user flow.

In today’s tutorial we are going to be writing integration tests for the registration and confirmation flows we have been implementing over the last couple of weeks.

What are Integration Tests?

Integration tests test the application from the outside in. These tests usually test a complete flow that a user will take. For example registering as a new user. This will typically involve multiple requests and controllers, often using multiple areas of the application in a single test.

Integration tests are arguably the most important tests in your test suite as they confirm that the application works as it is expected to.

It’s important to concentrate on the right details at each layer of abstraction. For example, in an integration test you don’t need to confirm that every error is returned as expected.

Instead you should ensure that your business rules are enforced correctly. If you need to assert very specific details, I think it’s better to deal with that on a more granular level. In my case, I deal with this in at the unit level.

Generating a new Integration Test

Now that we have an understanding of integration tests, their purpose, and what you should be looking for, let’s get down to writing integration tests for the registration and confirmation flows of this application.

First we can use the Rails generator to generate test classes for each of the flows:

bin/rails generate integration_test registration_flows

I’m going to separate these two flows into two separate classes:

bin/rails generate integration_test confirmation_flows

These commands should generate a new empty integration test class. With the test classes in place, we can now start writing the integration tests.

Writing the Registration Integration Tests

The first integration tests I will write will be for registration. The test class that we generated should look like this to begin with:

require "test_helper"

class RegistrationFlowsTest < ActionDispatch::IntegrationTest
end

First I will write a test to make a request with invalid details:

test "attempt to register with invalid details" do
  get "/join"
  assert_response :success

  post_via_redirect "/join", user: {email: "", username: "", password: ""}
  assert_equal "/join", path
  assert_response 400
end

I’m not testing to assert that the errors are correct as I feel like I’ve adequately covered this at other levels of testing.

Next I will write a test that will attempt to register with existing user details:

test "attempt to register with existing user details" do
  user = User::Create::Operation::Default.(user: attributes_for(:user)).model

  get "/join"
  assert_response :success

  post_via_redirect "/join", user: {email: user.email, username: user.username, password: ""}
  assert_equal "/join", path
  assert_response 400
end

Again I’m not asserting the response as the fact that it returns with the correct HTTP status code despite having “valid” data is enough for me.

And finally I will attempt with valid details and I should expect that the request is successful:

test "register as new user" do
  get "/join"
  assert_response :success

  assert_difference "ActionMailer::Base.deliveries.size" do
    post_via_redirect "/join", user: {email: "name@domain.com", username: "name", password: "password"}
  end

  assert_equal "/login", path
  assert_response :success
end

So as you can see from this step, we are combining a couple of requests into this single flow test. First we test that the /join page loads correctly. Next we assert that the POST request is successful and that the email is sent correctly. And finally we assert that we are redirected to the right page on success.

This whole test class should look like this:

require "test_helper"

class RegistrationFlowsTest < ActionDispatch::IntegrationTest
  test "attempt to register with invalid details" do
    get "/join"
    assert_response :success

    post_via_redirect "/join", user: {email: "", username: "", password: ""}
    assert_equal "/join", path
    assert_response 400
  end

  test "attempt to register with existing user details" do
    user = User::Create::Operation::Default.(user: attributes_for(:user)).model

    get "/join"
    assert_response :success

    post_via_redirect "/join", user: {email: user.email, username: user.username, password: ""}
    assert_equal "/join", path
    assert_response 400
  end

  test "register as new user" do
    get "/join"
    assert_response :success

    assert_difference "ActionMailer::Base.deliveries.size" do
      post_via_redirect "/join", user: {email: "name@domain.com", username: "name", password: "password"}
    end

    assert_equal "/login", path
    assert_response :success
  end
end

Writing the Confirmation Integration Tests

Next I will write the confirmation integration tests. If you remember back to Building out a User Confirmation flow in Trailblazer this comprises of a request flow and a confirmation flow.

I’ll keep these as two separate classes, but in the same module to show that these are two flows of one bigger flow:

require "test_helper"

module ConfirmationFlowsTest
  class RequestFlowsTest < ActionDispatch::IntegrationTest
  end

  class ConfirmationFlowsTest < ActionDispatch::IntegrationTest
  end
end

Requesting a Confirmation email

Here are the tests for requesting a confirmation email:

test "attempt with invalid email" do
  get "/confirmation/request"
  assert_response :success

  post_via_redirect "/confirmation/request", user: {email: ""}
  assert_equal "/confirmation/request", path
  assert_response 400
end

test "attempt with not found email" do
  get "/confirmation/request"
  assert_response :success

  post_via_redirect "/confirmation/request", user: {email: "name@domain.com"}
  assert_equal "/confirmation/request", path
  assert_response 400
end

test "attempt with confirmed email" do
  user = User::Create::Operation::Confirmed.(user: attributes_for(:user)).model

  get "/confirmation/request"
  assert_response :success

  post_via_redirect "/confirmation/request", user: {email: user.email}
  assert_equal "/confirmation/request", path
  assert_response 400
end

test "attempt with unconfirmed" do
  user = User::Create::Operation::Default.(user: attributes_for(:user)).model

  get "/confirmation/request"
  assert_response :success

  assert_difference "ActionMailer::Base.deliveries.size" do
    post_via_redirect "/confirmation/request", user: {email: user.email}
  end

  assert_equal "/login", path
  assert_response :success
end

The nice thing about integration tests is the fact that you can pretty much just read them and understand what’s going on. The Rails DSL even makes it not too much of a leap for a non-technical person to grasp what each test is testing.

Once again in these tests I’m simply walking through each business rule to ensure the flow abides by the rule. The final test is the happy path.

Confirming a User

Finally, here are the integration tests for confirming a user:

test "attempt with invalid token" do
  assert_raises ActiveRecord::RecordNotFound do
    get "/confirmation/confirm?token=invalid"
  end
end

test "attempt with invalid imported user data" do
  user = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model

  get "/confirmation/confirm?token=#{user.confirmation_token}"
  assert_response :success

  post_via_redirect "/confirmation/confirm", token: user.confirmation_token, user: {
    username: "", password: ""
  }
  assert_equal "/confirmation/confirm", path
  assert_response 400
end

test "confirm imported user" do
  user = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model

  get "/confirmation/confirm?token=#{user.confirmation_token}"
  assert_response :success

  post_via_redirect "/confirmation/confirm", token: user.confirmation_token, user: {
    username: "name", password: "password"
  }
  assert_equal "/login", path
  assert_response :success
end

test "confirm default user" do
  user = User::Create::Operation::Default.(user: attributes_for(:user)).model

  get "/confirmation/confirm?token=#{user.confirmation_token}"
  assert_response :success

  post_via_redirect "/confirmation/confirm", token: user.confirmation_token
  assert_equal "/login", path
  assert_response :success
end

Again I’m simply walking through each business rule to ensure that it is satisfied. I’ve already tested the finer grain details of each business rules at the other layers of testing abstraction, and so these tests are more of a smoke test to ensure the flow is working correctly as a whole.

Conclusion

Over the last couple of months we’ve covered all of the various types of tests you will find yourself writing when building a well tested Ruby on Rails application.

Each layer of abstraction in your test suite is important and serves a role when writing good quality and maintainable tests.

I find that in order to make tests worth writing, easy to work with, and maintainable, it’s very important to test the correct details and each level of abstraction. However, these types of heuristics are often only discovered by walking down the wrong path and learning from your mistakes.

In either case, I hope this provides a little bit of insight into how I write my application tests!

Philip Brown

Hey, I'm Philip Brown, a designer and developer from Durham, England. I create websites and web based applications from the ground up. In 2011 I founded a company called Yellow Flag. If you want to find out more about me, you can follow me on Twitter or Google Plus.