Jul 06, 2016
Table of contents:
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.
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.
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.
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
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
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.
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.
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!