Blog

How we test our Rails projects (2/3)

Icône flèche bleue vers la gauche
Back to blog
How we test our Rails projects (2/3)

How we test our Rails projects (2/3)

May 8, 2016

Introduction

This blog post is the second of a series of 3 articles explaining how we write automatic tests for our Rails projects.

The 3 parts discuss the following topics :

Part 2 : The structure

/spec

Since we are using RSpec, we won’t be using the test folder but the spec one.

This folder will keep all our example files (an example is any kind of automatic test in RSpec terms), but also code and assets specific to our tests. More precisely there will be 3 subfolders dedicated to store files helping our examples and as many folders storing example files.

The 3 sub folders containing helping files are :

     
  • factories : Where we put FactoryGirl factory definitions. Usually one file by object tree we need to build.  
  •  
  • resources : Where we put static files we need in our examples. Those can be files we need to simulate an upload, files we need to compare with the result of a file generation, picture files we need to populate avatars of our users, etc. When we need a file to be used as-is without the need of parsing it, we will put it there.  
  •  
  • support : Where we put all the supporting code for our test environment. This is the biggest one of the three and has its own sub structure.  

The other subdirectories will mostly mimic the structure of your app folder. We usually follow a structure of stereotyped folders ( the rails way ) for the common code and topic folders for specific code. Thus if we have the subfolders app/models/user.rb, app/api_clients/twilio_client.rb, and app/invoicing/invoice.rb files, we will have the spec/models/user_spec.rb, spec/api_clients/twilio_client_spec.rb and spec/invoicing/invoice_spec.rb.

I said mostly because there is one additional folder in spec which does not appear in the app folder : the features folder.

/spec/features

This folder is where we put our feature specs.

We have tried using special tools like Cucumber for our feature specs. It turns out for us that the cost of an additional tool and an additional language is not worth the effort. Especially since, in our usual projects, the product owner almost never read the user stories, even less writes them; the project manager is responsible for translating the needs to the developer and the developer writes and maintains the stories. Therefore we wanted a tool making the task easy for the developer and the combination of RSpec, SitePrism and Capybara is the best we have found to enable the developers to feel at home.

This folder hosts the feature specs but what does our feature specs looks like ?

Our files (code and specs) are always structured to hoist the important parts at the top of the file and the boilerplate at the bottom. For our feature specs this means that the file has a short name matching its file name allowing us to easily jump on them in our editor, then has a longer description in the form of a user story, then the code really begins.

The code starts with the inclusion of the required shared_contexts for setting up a coarse grained state of the app, then a background block calling some private methods for a more fine grained initial setup.

Then we have the scenarios. Each scenario has a short descriptive name allowing us to differentiate it from the other scenarios of the same feature (meaning a feature file with a single scenario can have a very simple name). The scenario starts by calling some private methods to put the application in the exact state required for that scenario. Then we have a succession of private method calls written to look like sentences. Active first person sentences are used to describe how the user interacts with the application. Passive sentences are used to describe assertions made in order to continue or fail the examples. Other active sentences are used to describe external actions happening.

This technique is very simple and gets almost all the benefits from tools like cucumber without the hassle of them. As developers we are not at all disturbed by the snake case transformation of the sentences and the 3 simple gramatical rules enable us to immediately know what kind of step we are in.

After the scenarios we enter the private area where we can have test values in the form of let() calls and the definition of all the private methods we have called earlier.

Here is an example illustrating all these concepts :

We imagine a story where a new client must have its ticket payment validated before downloading its concert ticket.

# /spec/features/fresh_client_want_to_download_
require 'rails_helper'

RSpec.feature 'a user who has bought ticket using bank transfer want to download its ticket', '
 As a User having bought concert tickets using bank transfer
 In order to be able to enter in the concert room
 I want to download my tickets
' do

 include_context 'users exist'
 include_context 'concert exist'
 include_context('a ticket has been bought using bank transfer') do
   let(:ticket_concert){ concert }
   let(:ticket_buyer){ client }
 end

 background do
   free_some_seats_for_the_concert
 end

 scenario 'I can download my tickets after an admin validation', :js  do

   as(client) do
     i_go_to_my_dashboard
     i_go_to_the_details_of_my_account
     my_ticket_is_not_available_until_my_payment_has_been_validated
   end

   as(admin) do
     i_go_to_the_admin_panel
     i_display_the_first_pending_payment
     i_validate_the_bank_transfer_payment
   end

   as(client) do
     i_follow_the_email_annoucing_me_my_ticket_is_available
     i_download_my_ticket
   end

   one_less_seat_is_available_for_the_concert

 end

 private
 ...
end  

As you can see, the scenario is quite easy to follow and when we read a sentence we can immediately know if it is a user action step ( i_go_to_my_dashboard ), an assertion step (my_ticket_is_not_available_until_my_payment_has_been_validated) or an external action step ( free_some_seats_for_the_concert ).

The question you are probably asking yourself is : “But where is the code ?”. The code appears after the private keyword. we have a list of let() statement for when a value needs to be used in multiple steps but we don’t really care about what it is ( let(:street_name){ 'Boulevard Acme' }), and a lot of very small private methods translating those steps into method calls like these :

def i_go_to_my_dashboard
 navbar.visit_dashboard
end

def i_follow_the_email_annoucing_me_my_ticket_is_available
 last_email = mailbox_for(client.email).last
 expect(last_email.subject).to eq I18n.t("ticket_mailer.available_ticket.subject")
 click_email_link_matching(%r'my/tickets', last_email)
end

Ok but where do those as(), navbar, client, … methods come from ? You haven’t told us everything !

Not yet, but here come the best part …

/spec/support

This folder contains all the supporting code for our examples, it will enable us to :

     
  • Setup very easily the application in a complex state thanks to shared_contexts.  
  •  
  • Extend the Rspec DSL with some useful new keywords thanks to helpers (use them with caution).  
  •  
  • Override some behaviour in order to better isolate some components when we want to test them in isolation (testing a User record should require ActiveRecord, fair enough but not the complete Rails app juste because of Devise).  
  •  
  • Share page manipulation logic between multiple scenarios thanks to SitePrism page objects.  

/spec/support/initializers

The first support subfolder we are going to create is /spec/support/initializers. The purpose of this folder is to mimic /config/initializers in the sense that all of its files will be loaded during the boot process of our app. Usually one puts RSpec configuration in /spec/spec_helper.rb and Rails specific RSpec configuration in /spec/rails_helper.rb. We do put RSpec configuration in /spec/spec_helper.rb but we split /spec/spec_helper.rb config in multiple files stored in /spec/support/initializers folder. This allows us to keep /spec/rails_helper.rb smaller, to have topic config files and to have other spec helper files loading only a subset of the initializers (typically we have an /spec/activerecord_spec_helper loading only what is required to test activerecord objects).

Here is what a fresh project starts with most of the time :

capybara.rb : setup capybara and its drivers. We mostly uses poltergeist and rack-test drivers. we configure autoscreenshot upon failure and check ENV variables or metadate for diver overrides.

#/spec/support/initializers/capybara.rb

require 'launchy'
require 'capybara'
require 'capybara/poltergeist'
require 'capybara/rails'
require 'capybara/rspec'
require 'selenium-webdriver'
require 'capybara-screenshot'
require 'capybara-screenshot/rspec'

Capybara.register_driver :poltergeist do |app|
 Capybara::Poltergeist::Driver.new(
     app,
     js_errors: true, # break on js error
     timeout: 180, # maximum time in second for the server to produce a response
     debug: false, # more verbose log
     window_size: [1280, 800], # not responsive, used to simulate scroll when needed
     inspector: false, # use debug breakpoint and chrome inspector
     phantomjs_options: ['--ignore-ssl-errors=yes'] ,
     # extensions: [Rails.root.join('spec', 'support', 'phantomjs_ext', 'geolocation.js').expand_path.to_s]
 )
end


Capybara.register_driver :chrome do |app|
 Capybara::Selenium::Driver.new(app, :browser => :chrome)
end


Capybara.register_driver :firefox do |app|
 Capybara::Selenium::Driver.new(app, :browser => :firefox)
end


Capybara.javascript_driver = ENV.fetch('DRIVER') { 'poltergeist' }.to_sym
Capybara.asset_host = 'http://localhost:3000' # enable asset in save_and_open_page if we have a dev server running


Capybara.configure do |config|
 config.default_selector = :css
 config.default_max_wait_time= 2
 config.match = :prefer_exact
 config.ignore_hidden_elements = true
end

[:chrome, :firefox, :poltergeist].each do |driver|
 RSpec.configure do |config|
   config.around(:each, driver) do |example|
     current_javascript_driver = Capybara.javascript_driver
     current_default_driver = Capybara.default_driver
     Capybara.javascript_driver = driver
     Capybara.default_driver = driver
     example.run
     Capybara.javascript_driver = current_javascript_driver
     Capybara.default_driver = current_default_driver
   end
 end
end


Capybara.save_and_open_page_path = Rails.root.join('tmp', 'save_and_open_page')
Capybara::Screenshot.prune_strategy = { keep: 10 }

Capybara::Screenshot.register_driver(:chrome) do |driver, path|
 driver.browser.save_screenshot path
end

database_cleaner.rb : We configure database cleaner in a very simple fashion in order to either use transaction strategy during activerecord and simple feature specs , or truncation during javascripted feature specs. This file is really project dependent and this example is a simple one. Usually we add here seed management: data coming from the seed are loaded once at the beginning and excluded from the cleaning process.

#/spec/support/initializers/database_cleaner.rb

require 'database_cleaner'

RSpec.configure do |config|

 config.before(:suite) do
   DatabaseCleaner.strategy = :transaction
   DatabaseCleaner.clean_with :truncation
 end


 config.before(:each, type: :feature) do

   driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test

   if driver_shares_db_connection_with_specs
     DatabaseCleaner.strategy = :transaction
   else
     DatabaseCleaner.strategy = :truncation
   end
 end

 config.before(:each) do
   DatabaseCleaner.start
 end

 config.append_after(:each) do
   DatabaseCleaner.clean
 end

end

factory_girl.rb : We add a very small patch to factory girl allowing us to use file from the spec/resources folder. We use this in combination with Carrierwave uploaders.

#/spec/support/initializers/factory_girl.rb
require 'factory_girl'

module FactoryGirl::SyntaxSugar

 def resources_path(*parts)
   Pathname(File.join(File.realpath(__FILE__), '..', '..', '..', 'resources', *parts)).expand_path
 end

 def resources_file(*parts)
   File.new resources_path(*parts)
 end

end
FactoryGirl::SyntaxRunner.include FactoryGirl::SyntaxSugar

RSpec.configure do |config|
 config.include FactoryGirl::Syntax::Methods
 config.include FactoryGirl::SyntaxSugar

 config.before(:suite) do
   FactoryGirl.factories.clear
   FactoryGirl.sequences.clear
   FactoryGirl.find_definitions
 end

end

site_prism.rb : We add a patch to site prism allowing us to locate elements based on the data attribute data-purpose. This allows our team to work effectively between front and back developers. The backend developer must not rely on anything else than this attribute to locate an element. The front developer can change anything on the page but those attributes. It also enables us very effectively to not rely on - translateable - texts on the page.

#/spec/support/initializers/site_prism.rb
require 'site_prism'

SitePrism.configure do |config|
 config.use_implicit_waits = true
end

module  SitePrismExtension

 def purpose_element(purpose, options)
   element purpose, %Q'[data-purpose="#{purpose}"]', options
 end

 def purpose_elements(purpose, elements_name=purpose.to_s.pluralize)
   elements elements_name, %Q'[data-purpose="#{purpose}"]'
 end

 def purpose_section(purpose, section_class)
   section purpose, section_class, %Q'[data-purpose="#{purpose}"]'
 end

 def purpose_sections(purpose, section_class, elements_name=purpose.to_s.pluralize)
   sections elements_name, section_class, %Q'[data-purpose="#{purpose}"]'
 end
end

SitePrism::Page.send :extend, SitePrismExtension
SitePrism::Section.send :extend, SitePrismExtension

then we can add the following line in spec/rails_helper.rb

Dir[Rails.root.join('spec/support/initializers/*.rb')].each { |f| require f }

/spec/support/helpers

In an Object Oriented world, a function is something quite uncommon. Very often they are a bad idea, especially helper functions. This folder will nonetheless expose some of those functions. They won’t be real functions but mostly extensions to objects already injected in some contexts.

We usually use the following helpers :

capybara_helpers.rb : mostly acronyms because we are lazy and some method are way too long to be written when debugging. Also some component manipulation logic, in this example methods to use jquery-datetimepicker and select2 but you should change it to what you need.

#/spec/support/helpers/capybara_helpers.rb

require 'site_prism'
require 'capybara/rails'
require 'capybara/poltergeist'

module Capybara::AliasHelper

 def snap
   screenshot_and_open_image
 end

 def saop
   save_and_open_page
 end

 def resources_path(*parts)
   Pathname(File.join(File.realpath(__FILE__), '..', '..', '..', 'resources', *parts)).expand_path
 end

end

module  Capybara::ComplexInputsHelpers

 def pick_date(value, element)
   element.set(value.to_s)
   if Capybara.current_driver != :rack_test && element['id'].present?
     execute_script(%Q|$('##{element['id']}').datetimepicker('hide')|)
     sleep(0.2)
   end
 end

 def select_from_select2(value, select_container)
   select_container.find(".select2-selection").click # display filter
   find(".select2-search__field").set(value) # type text in filter
   find("li.select2-results__option--highlighted").click # choose option
   element_does_not_exist?("li.select2-results__option--highlighted")
 end
end

RSpec.configure { |c| c.include Capybara::AliasHelper }
RSpec.configure { |c| c.include Capybara::ComplexInputsHelpers }

SitePrism::Page.send :include, Capybara::AliasHelper
SitePrism::Page.send :include, Capybara::ComplexInputsHelpers
SitePrism::Section.send :include, Capybara::AliasHelper
SitePrism::Section.send :include, Capybara::ComplexInputsHelpers

page_objects_helper.rb: As explained in the previous article, we use SitePrism to write page objects. Those page objects are stateless and therefore a feature spec shouldn’t care about their instantiation. We use this file to expose each page object through its own private method and use memoization to be gain a small performance optimization.

We use the Memoist gem to do the job , not because we are too lazy to do the code by ourselve but because we like the idea of separated responsibility and memoization is a performance responsibility different than the business logic it applies to. So we think using the memoize macro describe better our intent than doing the memoization by hand.

#/spec/support/helpers/page_objects_helper.rb
module PageObjectsHelper
 extend Memoist

 def self.page_object(helper_method, page_class)
   define_method(helper_method) do
     page_class.new
   end
   memoize helper_method
 end

 page_object :navbar, Spec::Pages::Navbar
 page_object :home_page, Spec::Pages::HomePage
 page_object :login_page, Spec::Pages::LoginPage
 # And so on ...
end


RSpec.configure { |c| c.include PageObjectsHelper, type: :feature }

usual_steps_helper.rb : the magic as() helper. The code might need change in your project but it’s almost certain that you have scenario requiring someone to be logged in - or multiple persons. The point here is to separate everyone in its own session and leveraging the login_page login to do the authentication.

If you have blank screenshots when using this snippet, ensure you use a version of capybara-screenshot including this fix : https://github.com/mattheworiordan/capybara-screenshot/pull/132

#/spec/support/helpers/usual_steps_helper.rb

module UsualStepsHelper

 def as(user, options={})
   experimental = options[:experimental] || false
   session_name = "capybara_session_for_#{user.to_param}"
   using_session(session_name) do
     visit new_user_session_path
     login_page.login user.email, user.password
     force_experimental_cookie if experimental
     yield
     navbar.logout
   end
 end

 def as_guest
   session_name = "capybara_session_for_guest-#{rand(1_000_000)}"
   using_session(session_name) do
     visit '/'
     yield
   end
 end

end

RSpec.configure { |c| c.include UsualStepsHelper, type: :feature }

/spec/support/overrides

The last folder contains the most problematic kind of code, patches. When writing tests, you typically want to assert how your code is behaving. Monkey patching your code in a test environment means you are asserting a different behaviour than the normal one, thus missing the entire point of the test.

BUT

In some circumstances, testing the real behaviour is not your responsibility, and you should not do it. I’m talking about when the use of a library or framework asks you to write some code in your object making it a slave of two masters : You and the library writer.

In those cases, you might want to patch your object to drop its relation to the library in order to test only the behaviour you have designed. It’s a fragile thing since you’re not testing the real object but it might still make sense when integrating tools from which an isolation layer is really hard and/or stupid to write.

One of those cases is Devise.

When testing a User object, or an object graph containing a User instance, We very often do not want anything Devise related. Devise is meant to be integrated directly into the User class code and in Order to remove it, we use the following patch :

#/spec/support/overrides/devise.rb

require 'active_record'

module DeviseInhibitor
 def devise(*whatever)
   attr_accessor :password, :password_confirmation
 end
end

ActiveRecord::Base.singleton_class.prepend DeviseInhibitor

Then we require this specific file in the example file.

When using such overrides , we always require them manually so it’s very explicit that the test is against a slightly drifted variation of the code.

We already have used this technique with gems like geocoder, too which has the same integration pattern as devise.

/spec/support/shared_contexts

This folder is simply where we put globally known shared contexts. When a shared context is meant to be used in any kind of feature spec (like “Users with different roles exists”, or “A concert will happen next week”) we put the file here. We use shared_contexts as a kind of very powerful data fixtures : creating data and helper methods to access those data.

Here is an example following the previous feature spec example. including this context create the expected records and allows us to use the let statement to access the data easily.

# /spec/support/shared_contexts/ticket_has_been_bought.rb

RSpec.shared_context 'a ticket has been bought using bank transfer' do

 let(:ticket_concert){ fail "please define the concert for which this ticket has been bought" }
 let(:ticket_buyer){ fail "please define the user who has bought the ticket" }


 let(:ticket_price){ 100 }
 let(:vip_kind){ TickeType.vip } # we imagine those records are parts of the seed

 let!(:ticket_transaction) do
   # we imagine having a factory for this record
   create(:ticket_transaction,
     :bank_transfer,
     :pending
     issued_at: 3.days.ago
     ....
   )
 end

 let!(:ticket) do
   # we imagine having a factory for this record
   create(:ticket,
     :signed,
     user: ticket_buyer,
     concert: ticket_concert,
     price: ticket_price,
     transaction: ticket_transaction,
     ....
   )
 end


end

/spec/spec_helper.rb, /spec/rails_helper.rb, /spec/activerecord_spec.rb

/spec/spec_helper.rb is the smallest, fastest initializer we can write for rspec. We use all the recommanded default setting, and add just 3 additional things :

     
  • Collection Matchers : Because we like their expressivity very much, and since all our rspec related gems are require => false in our gemfile , we require them here.  
  •  
  • Auto looading of usual rails files : Even when we do not start Rails we like being able to benefit from the autoloading of constants from files in the app directory.  
  •  
  • Defaulting the time zone : Because we have found the hard way that problems coming from machines having different default time zones are a pain to discover.  

So here is what we add to the default file :

# /spec/spec_helper.rb
require 'rspec/collection_matchers'

require 'active_support'
require 'active_support/core_ext'
require 'active_support/dependencies'

Dir[File.join(File.dirname(__FILE__), '..', 'app', 'models')].each { |f| ActiveSupport::Dependencies.autoload_paths << f }
Dir[File.join(File.dirname(__FILE__), '..', 'app', '*')].each { |f| ActiveSupport::Dependencies.autoload_paths << f }
Dir[File.join(File.dirname(__FILE__), '..', 'app', '*', 'concerns')].each { |f| ActiveSupport::Dependencies.autoload_paths << f }

Time.zone_default= Time.find_zone!('UTC')

/spec/rails_helper.rb is the heaviest, initializer we can write for rspec. It boots the entire rails application and is used for feature spec. We load some rspec extensions we only have meaning for those kind of specs (i.e. webmock), and we load all the files described above (except the overrides of course).

We also set the default rails application locale, in order to be sure that the test process and the server process have the same default. So here is our file :

# /spec/rails_helper.rb

ENV['RAILS_ENV'] ||= 'test'

require 'spec_helper'
require File.expand_path('../../config/environment', __FILE__)
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
require 'webmock'
require 'webmock/rspec'
WebMock.disable_net_connect!(:allow_localhost => true)

# Include all our custom setup
Dir[Rails.root.join('spec/support/initializers/*.rb')].each { |f| require f }
Dir[Rails.root.join('spec/support/pages//*.rb')].each { |f| require f }
Dir[Rails.root.join('spec/support/helpers/*.rb')].each { |f| require f }
Dir[Rails.root.join('spec/support/shared_contexts/*.rb')].each { |f| require f }

ActiveRecord::Migration.maintain_test_schema!

RSpec.configure do |config|
 config.use_transactional_fixtures = false
 config.infer_spec_type_from_file_location!
end

I18n.locale= :fr

/spec/activerecord_spec.rb is a custom helper for testing objects in almost isolation. We use a lot of plain old ruby objects in our apps but we do not want to completely isolate activerecord form the rest of the app only because it is tightly tied to a third party library. We accept this bound and follow the active record pattern : adding behaviour to objects representing database records. But then we need to test them of course and this doesn’t mean we want to load the entire rails application with its routing, controllers, views, assets, etc.

This is why we have this third helper :

#/spec/activerecord_spec.rb
require 'spec_helper'

require 'active_record'
require 'factory_girl'
require 'yaml'

db_configurations = YAML::load(ERB.new(File.read("config/database.yml")).result)

ActiveRecord::Base.send(:configurations=, db_configurations)
ActiveRecord::Base.establish_connection(:test)

require_relative "support/initializers/factory_girl"
require_relative "support/initializers/database_cleaner"

require_relative 'support/overrides/devise'

Conclusion

This one is a long post but we hope you will find it interesting not only to understand the way we organize our files but also the real code we use. Those files are the result of a long path of small improvements and are in perpetual changes. There hasn’t been any major change in the structure for a long time but every project we work on brings its new contraints and solutions.

See you soon for the next and last article, The flow, where we will explain how we go from test to code and vice versa.

Update : The next and last part is ready, you can find it here

Ready to build your software product? Contact us!