Smart Integration Testing in Rails

When we start to talk about testing we always seem to end up discussing integration testing. But is integration testing really our favourite superhero? Or can it become our worst enemy? In this post I'm going to try, using a practical example, to work out how far we can take integration testing and when it would be better to delegate testing to other parts of the application.

Estimate reading 3 minutes

Smart integration tests with RSpec

Recently, I've been working on a project for a business client. It's all very standard. An administrative backend combined with a public frontend.

However, one model in particular caught my attention: the representation of a piece of real estate.

The related database table contains around 60 attributes. Which is a lot, I know. In this case, we can break the model down into numerous _sub-models_:

Breaking down the model in this way can make it easier to work with, though it can also lead to some problems with forms... but we'll talk about that later.

And whether we break the model down or not, we're still going to be left with 60 fields.

Integration testing

This project is relatively simple, so I've decided to use it as a test case for identifying ways of improving and strengthening integration testing.

What I want to experiment with is an approach to testing based on the (almost) exclusive use of integration testing, one which makes it possible to check a good proportion of the business code using just a few lines of test code.

The first thing we need to do is think about the scenarios that we have to test for. One approach would be the following (in which the "Pages" module makes use of the SitePrism gem):

Now, if we want to use an entirely Behaviour-driven Development (BDD) approach, every line of code should be anticipated by a test which justifies its existence.

If we choose to follow this strategy, while using only integration testing, we're going to have to replicate this code for every field (or at least for every field that's in need of some kind of validation).

That's something you may soon come to regret! When the test code expands in this way it can become a hassle to maintain, which leads to the risk of test writing being abandoned entirely.

Can we do better?

Let's think for a second about the controller. Within our code, the controller is the point that represents actions a user can carry out in the application:

Setting things up in this way, it's unlikely that we'll need to the deal with the features again. Furthermore, it will be possible to check a new field using at most a few lines of code, making the tests a lot simpler to maintain.

Shared Examples

Using shared examples we can abstract different scenarios, allowing us to reuse the code across all of the application's resources:

RSpec.shared_example `a resource you can create` do
describe `Creating an entry` do
scenario `with valid values` do
new_page.load
fields.each do |field, value|
new_page.send("#{field.to_s}_field").set value
end
new_page.submit!
expect(new_page).to have_notice
end
scenario `with invalid values` do
new_page.load
new_page.submit!
expect(new_page).to have_alert
end
end
end

Search filters

Every index of a data resource should contain a form that makes it possible to filter its associated records.

Here again, the approach of creating an individual scenario for each and every filter could lead to a great deal of avoidable toil.

Ultimately, there are three options:

I want to see all of the records without passing any parameters (an index will usually behave this way by default)

I want to pass all of the parameters and get a particular record

I don't want to get any records and I want to pass all of the parameters

describe `When I filter RealEstates` do
given(:index_page) { Pages::RealEstates::Index.new }
given(:record) { create(:real_estate, :with_all_fields) }
before { index_page.load }
scenario `withour any search field I find the record` do
expect(index_page).to have_record(record)
end
scenario `with existing values I find the record` do
# here i fill the form
expect(index_page).to have_record(record)
end
scenario `with not existing values I do not find any record` do
#here I fill the form
expect(index_page).to_not have_record(record)
end
end

Now, using more or less the same approach that we used with the creation of the real estate representation, we will delegate all of the resource's scope methods.

So, we can do everything with integration testing?

The answer to this question is: "Yes, we can: but it requires a lot of effort and will make the code writing process rather tedious."

I also ought to say that I had to go through many, many hours of writing, deleting and rewriting code before I could find a solution that was able to make testing almost fun.

If we never have to deal with the world of RSpec (or with that of testing in general) it will take us a lot of time to think up ways of structuring our single tests, and we will in all likelihood prefer to do manual checking using a browser.

David Librera

Software engineer, DevOps

Computer Engineer, Rails developer with a lot of experience in the world of web-scraping and database lover.