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  
[/bash]

I’m going to separate these two flows into two separate classes:  
```bash  
bin/rails generate integration_test confirmation_flows  
[/bash]

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:  
```ruby  
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!