Just because nobody complains doesn’t mean all parachutes are perfect
– Benny Hill.
WHY TEST THE USER PERSPECTIVE WITH RUBY ON RAILS?
A month ago we published an article about the value of automated testing. The post was focused on describing various ways how automated testing can help with ensuring quality of your Ruby on Rails application. With the article that follows we move our attention from discussing pros of automated testing to discussing best practices for implementing automated testing in a Ruby on Rails application.
First, I will demonstrate how to setup basic automated test suite environment for a Ruby on Rails platform. Next, we will write a single end-to-end test which goal will be to ensure that our RoR application works as expected, from the user perspective. The test will also validate features integration within the system, telling us if the user can achieve his / her goals by using the application interface. Developers call such features integration test a “happy path” A “happy path” it is usually a scenario which takes a virtual user for a journey through the application features, focusing on those that are expected to be most used. Furthermore, a “happy path” test scenario gives confidence that the system works well under typical conditions.
After we finish writing the end-to-end test, we will add a few supportive tests to our test suite. The goal of supportive tests is to verify alternative flows and other important features which have not been covered by the “happy path” test, e.g. access control.
All mentioned benefits of end-to-end and supportive tests come from taking user perspective approach during the process of designing automated test suite.
PREPARATIONS
The application used for demonstrating automated testing from user perspective isa simple project / task management app written in Ruby on Rails. A “happy path” in this application could be described by a situation where an user logs in and creates a project. The code of aforementioned RoR application can be found at tests-with-kameleon repository. After cloning the repository you can write your own tests on the master branch. However, if you want to see the tests implemented along the lines advocated in this article, switch to the with-tests branch.
From among a number of ruby gems, I have chosen Kameleon to work with. Kameleon is basically a dsl for writing expressive acceptance and integration tests. Let me how we can setup our project / task management RoR application with Kameleon
Setup
Assuming that you have initialized the project and that you are using Bundler gem, the first thing you do is add the gems for the test environment. Add the following code in your Gemfile:
group :test, :development do
gem 'rspec-rails'
end
group :test do
gem 'kameleon' # dsl for high abstracted test writing, based on capybara
gem 'factory_girl_rails' # fixtures replacement
gem 'capybara' # dsl for test writing
gem 'spork', '~>;; 1.0rc' # test server
gem 'database_cleaner' # set of strategies for cleaning your database
end
Then you run
bundle install
The Gems have been installed and now it’s time to configure them.
RSpec
To generate basic rspec files run the command:
rails generate rspec:install
spork
To bootstrap your test helper file, run the command:
spork rspec --bootstrap
Next add a –drb parameter to the .rspec file. This will enable the tests to be executed faster thanks to a spork server.
You can now complete the spork configuration by editing the spec/spec_helper.rb. The configuration used for the purpose of this article looks like this:
require 'rubygems'
require 'spork'
Spork.prefork do
ENV\["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", \_\_FILE\_\_)
require 'rspec/rails'
require 'kameleon/ext/rspec/all'
Dir[Rails.root.join("spec/support/*_/_.rb")].each {|f| require f}
RSpec.configure do |config|
config.mock_with :rspec
config.include FactoryGirl::Syntax::Methods
config.before(:all) do
DatabaseCleaner.clean_with(:truncation)
end
end
end
Spork.each_run do
end
The code above is responsible for requiring Rubygems, Spork, RSpec, Kameleon and some other helpers, which are placed in the support folder. In the RSpec.configure block we pass the configuration parameters for RSpec. We use config.before(:all) block to indicate that the database should be cleaned once before running examples from a certain group.
Create fixtures (optional)
For the purpose of this article I have decided to use the Factory Girl gem to make fixtures. Our spec/factories.rb file looks like this:
FactoryGirl.define do
factory :user do
email 'some.user@example.com'
password 'secret'
end
factory :project do
active true
end
end
4. Set up the test database
Run
RAILS_ENV=test rake db:setup
in command line
Run tests
Run spork in the first terminal window and, after it has been loaded, run rspec spec in the second one. If everything goes well, you should see “No examples found (…) 0 examples, 0 failures“. It’s time to write the first test.
Write an end-to-end test
In our end-to-end scenario we will create three users (Stewie, Chris and Peter), who will sign in into the application. Stewie will create a project and then a task which will be delegated to Chris. Chris will update the task progress and delegate task to Peter for feedback. Peter will close the task. At the end Stewie will remove the closed task, rename the project and, finally, delete it.
In order to create multiple sessions for the test, I have added some helper methods in the spec/support/sessions.rb file:
def create_users(config)
@session_to_user_attributes_map ||= {}
config.each do |session_name, attributes|
@session_to_user_attributes_map\[session_name] = attributes
create(:user, attributes)
end
end
def create_sessions
@session_to_user_attributes_map.each do |name, user_attributes|
act_as(name) do
visit new_user_session_path
fill_in user_attributes\[:email] =>;;'Email', user_attributes\[:password] =>;;'Password'
click 'Sign in'
end
end
end
In this file: create_users method will create User objects and establish a capybara session for each of the user objects. *We should run it once, before all the tests in the group*. The create_sessions method simply logs in all previously defined users. It should be run before each example.
Now, let’s create the spec/requests/end_to_end_spec.rb file, and write some tests:
require 'spec_helper'
describe 'Application' do
before(:all) do
create_users({
:stewie =>;; {:email =>;; 'stewie.griffin@example.com', :password =>;; 'secret'},
:chris =>;; {:email =>;; 'chris.griffin@example.com', :password =>;; 'secret'},
:peter =>;; {:email =>;; 'peter.griffin@example.com', :password =>;; 'secret'}
})
end
before(:each) { create_sessions }
it 'allows users to complete project with many tasks' do
# creating project
act_as(:stewie) do
click 'Projects', 'New project'
fill_in 'Ruby Project' =>;; 'Name', :check =>;; 'Active'
click 'Create Project'
see 'Project created!', 'Ruby Project'
end
# creating task
act_as(:stewie) do
click 'Projects', 'Ruby Project', 'New task'
fill_in 'Setup test env' =>;; 'Name',
'Use kameleon with rspec' =>;; 'Description',
:select =>;; { 'chris.griffin@example.com' =>;; 'User'},
:choose =>;; 'High'
click 'Create Task'
see 'Task created!'
within('table') do
see 'Setup test env', 'New'
end
end
# update task
act_as(:chris) do
visit root_path
click 'Setup test env'
see 'Status: New', 'Progress: 0%', 'Use kameleon'
click 'Edit task'
fill_in 'I have installed rspec, spork, capybara and kameleon. Please review configuration.' =>;; 'Comment',
:select =>;; { 'peter.griffin@example.com' =>;; 'User', '50%' =>;; 'Progress', 'Opened' =>;; 'Status'}
click 'Update Task'
see 'Task updated!'
within('table') do
see 'Setup test env', 'Opened'
end
end
# update task #2
act_as(:peter) do
visit root_path
click 'Setup test env'
see 'Status: Opened', 'Progress: 50%', 'Use kameleon', 'I have installed rspec'
click 'Edit task'
fill_in 'It is correct. I am closing this task' =>;; 'Comment', :select =>;; { 'stewie.griffin@example.com' =>;; 'User', '100%' =>;; 'Progress', 'Closed' =>;; 'Status'}
click 'Update Task'
see 'Task updated!'
within('table') do
see 'Setup test env', 'Closed'
end
end
# remove task, update project, remove project
act_as(:stewie) do
visit root_path
click 'Projects', 'Ruby Project', 'Setup test env'
click :and_accept =>;; 'Remove task'
see 'Task destroyed!'
not_see 'Setup test env'
click 'Edit project'
fill_in 'PROJECT TO DESTROY' =>;; 'Name',
:uncheck =>;; 'Active'
click 'Update Project'
see 'Project updated!'
click 'PROJECT TO DESTROY'
click :and_accept =>;; 'Remove project'
see 'Project destroyed!'
not_see 'PROJECT TO DESTROY'
end
end
end
First, we have to require spec_helper.rb. In the describe ‘End to end test’ block we use a before(:all) block to create users and establish capybara sessions for them. We log in each user created in the before(:each) block. In the it ‘works’ block we make assertions in order to verify if the data displayed to users is correct. The acts_as blocks allow us to work with many sessions without the necessity to sign out / sign in each time we need it.
The test is composed of steps written in Kameleon DSL. As you can see, calling one method with multiple parameters usually allows us to perform a number of actions or assertions. The technique requires significantly less code than in case of the same test written in Capybara.
Run the tests again
Restart spork in the first terminal window (it has to load added spec/support/*.rb files) and run rspec spec in the second terminal window again. If everything goes well, you should see “1 example, 0 failures“. Well done!
Write supportive tests
When we have a passing end-to-end test at our disposal, it is a good idea to develop a few supportive tests to make sure that alternative flows and other features work fine as well. I have created two groups of examples – one connected with projects and the other with tasks. In the case of projects, I would like to test three things:
– if managers can see their projects on the dashboard,
– if validations work correctly during project creation,
– if access control works correctly for project management;
As regards tasks, there are also three things I would like to test:
– if the user assigned to a certain task can see his task on the dashboard,
– if validations work correctly during task creation,
– if access control works correctly for task management.
Let’s do the coding!
File: spec/requests/projects_spec.rb
require 'spec_helper'
describe 'Project' do
before(:all) do
create_users({:stewie =>;; {:email =>;; 'stewie.griffin@example.com', :password =>;; 'secret'},
:chris =>;; {:email =>;; 'chris.griffin@example.com',:password =>;; 'secret'}})
@project = Project.create :name =>;; 'Some project', :manager =>;; User.find_by_email('stewie.griffin@example.com')
end
before(:each) { create_sessions }
it 'should be displayed in managed list for its creator' do
act_as(:stewie) do
visit root_path
see 'Some project'
end
act_as(:chris) do
visit root_path
not_see 'Some project'
end
end
it 'must have a name and user assigned' do
act_as(:stewie) do
visit root_path
click 'Project', 'New project', 'Create Project'
within('#project_name_input') { see 'can't be blank' }
end
end
it 'should be editable only for project manager' do
act_as(:stewie) do
visit edit_project_path(@project)
not_see 'Access denied'
end
act_as(:chris) do
visit edit_project_path(@project)
see 'Access denied'
end
end
end
File: spec/requests/tasks_spec.rb
require 'spec_helper'
describe 'Task' do
before(:all) do
create_users({:stewie =>;; {:email =>;; 'stewie.griffin@example.com', :password =>;; 'secret'},
:chris =>;; {:email =>;; 'chris.griffin@example.com', :password =>;; 'secret'},
:peter =>;; {:email =>;; 'peter.griffin@example.com', :password =>;; 'secret'}
})
@project = Project.create :name =>;; 'Some project', :manager =>;; User.find_by_email('stewie.griffin@example.com')
@task = @project.tasks.create :name =>;; 'Testing task', :user =>;; User.find_by_email('chris.griffin@example.com')
end
before(:each) { create_sessions }
it 'should be displayed in delegated list for assigned user' do
act_as(:stewie) do
visit root_path
not_see 'Testing task'
end
act_as(:chris) do
visit root_path
see 'Testing task'
end
end
it 'must have a uniq name and assigned user' do
act_as(:stewie) do
visit root_path
click 'Project', 'Some project', 'New task', 'Create Task'
within('#task_name_input') { see 'can't be blank' }
within('#task_user_input') { see 'can't be blank' }
end
end
it 'should be editable only for project manager or assigned user' do
act_as(:stewie) do
visit edit_project_task_path(@project, @task)
not_see 'Access denied'
end
act_as(:chris) do
visit edit_project_task_path(@project, @task)
not_see 'Access denied'
end
act_as(:peter) do
visit edit_project_task_path(@project, @task)
see 'Access denied'
end
end
end
Re-run the tests
Restart spork in the first terminal window (it has to load added spec/support/*.rb files) and, subsequently, run rspec spec in the second terminal window again. If everything goes well, you should see “7 examples, 0 failures“. Very good, again!
Conclusion
Thus we have developed a nice set of automated tests for a simple Ruby on Rails application. Thanks to a good combination of RSpec and Kameleon, we have managed to write an expressive end-to-end test as well as six supportive tests in no time at all. All the examples written use a virtual browser, which has an influence on the execution time of the whole test suite. Your tests should execute fast. When you have a large number of tests and you do not have enough time to wait to for the full feedback, use tags to execute only end-to-end tests. As mentioned, their purpose is to ensure that the application will work correctly for the user under typical conditions.
Post Scriptum
Special thanks to Radek Jędryszczak and Michał Czyż for their feedback on this article.