In this post we'll look into creating a simple application with nested table. Nested tables allow to display additional information for nested sets of data (also known as trees or hierarchies) for relational databases. You could also apply this to any hierarchy based data format like MongoDB or simple JSON.
For this example we'll be showing the population of some european countries and cities to show the model relation country has_many :cities
.
This type of functionality is inspired from the popular Jquery plugin Data table.
I won't be getting into too much details about the app itself and we would be focusing instead on the test coverage. If you want to have a look at the complete app you can check out the repository on github. I have also added a set of seed data to make it more convenient. So if you run rake db:seed you will populate the db with some countries and cities.
Why bothering with testing?
Becoming a better developer will inevitably involve TDD (Test Driven Development) and any experienced Rails developer will heavily rely on testing to create and maintain error free code base. Testing as always been a part of coding but the Ruby community has embraced Automatic testing like any other. Testing establish additional trust when pushing your code to a team based project. It's simply about accountability. Also if you want to contribute to open source as some point you will need to provide your own tests.
There are 2 main testing frameworks when working with Ruby or Rails code:
- Minitest
comes packaged with Ruby itself and provide a quick way to
assert
that your code won't fail. You can learn a lot about Minitest while reading Micahel Hartl Rails tutorial. - RSpec is a "Behaviour Driven Development for Ruby. Making TDD Productive and Fun". Well I couldn't agree more with the RSpec catch phrase. This framework is trying to solve something important about testing: It should be more about your code than the test itself.
Some developer would also argue that you shouldn't be introducing a new language to test and you should be making your test using pure Ruby. I find that RSpec syntax is not difficult to learn and make your tests more maintainable. Anybody that doesn't know your app can understand what it should
expect
from your code. But don't just take my word for it and check out what feels more natural to you.
Setting up RSpec
I have been reading Everyday Rails Testing with RSpec and found it provides a clear and concise introduction to RSpec so you might recognize the set up.
First thing first, our new application:
rails new ajax_nested_table -T
-T will omit all the test framework as we are using RSpec.
Then let's add our gems and run bundle
. In the example below I have omitted the gems version number but you should check them individually on the rubygems.org website and lock them at their current stable version to avoid gem inconsistencies in the future.
group :development,:test do
gem 'rspec-rails'
gem 'factory_girl_rails'
end
group :test do
gem 'faker'
gem 'shoulda-matchers'
gem 'capybara'
gem 'database_cleaner'
gem 'launchy'
gem 'selenium-webdriver'
end
Now that we have all the gems needed to test with RSpec, let's install it with all the necessary structure:
rails generate rspec:install
Then include in config/application.rb
config.generators do |g|
g.assets false
g.helper false
g.test_framework :rspec,
fixtures: true,
view_specs: false,
helper_specs: false,
routing_specs: false,
controller_specs: true,
request_specs: false
g.fixture_replacement :factory_girl, dir: "spec/factories"
g.jbuilder false
end
Note that I am voluntarily omitting assets, helpers, test_framework and jbuilder in this config.
Finally, let’s install a binstub for RSpec:
bundle binstubs rspec-core
This will create an RSpec executable, inside the application’s bin directory.
At his stage we are all set with our testing framework and if you run
rspec
You'll get:
No examples found.
Finished in 0.00027 seconds (files took 0.07494 seconds to load)
0 examples, 0 failures
Testing our models
As mentioned above our app will show the population of some european countries and cities so let's first create our models:
rails g model Country name:string population:integer
rails g model City name:string population:integer country:references
And run
rake db:migrate
rails generator while creating our schema and models also added new files to spec:
spec
├── factories
├── models
| ├── country_spec.rb
| └── city_spec.rb
RSpec will know what file to test by using the following convention <<class name>>_spec.rb
.
Let's create our first test for our country model spec/models/country_spec.rb. We'll create a basic evaluation of our form validation.
require 'rails_helper'
describe Country do
it "is valid with a name and a population number" do
country = Country.new
country.name = "France"
country.population = 65447374
expect(country).to be_valid
end
it "is invalid without a name" do
country = Country.new(name: nil)
country.valid?
expect(country.errors[:name]).to include("can't be blank")
end
it "is invalid without a population number" do
country = Country.new(population: nil)
country.valid?
expect(country.errors[:population]).to include("can't be blank")
end
end
And if we run rspec
in our terminal the result is obviously a series of failed examples and one pending (our city spec that was created when running rails generator).
Finished in 0.01149 seconds (files took 2.92 seconds to load)
4 examples, 2 failures, 1 pending
Failed examples:
rspec ./spec/models/country_spec.rb:10 # Country is invalid without a name
rspec ./spec/models/country_spec.rb:15 # Country is invalid without a population number
And to pass this set of tests we'll need to add some validation to models/country.rb
class Country < ActiveRecord::Base
has_many :cities
validates :name, :population, presence: true
end
Now if we run:
rspec spec/models/country_spec.rb
We'll pass our firt test!!
Finished in 0.30295 seconds (files took 2.42 seconds to load)
3 examples, 0 failures
The process is pretty similar to test our city model spec/models/city_spec.rb we'll do:
require 'rails_helper'
describe City do
it "is valid with a name and a population number" do
france = Country.create(name: 'France', population: 65447374)
city = City.new(country_id: france.id)
city.name = "Paris"
city.population = 2240622
expect(city).to be_valid
end
it "is invalid without a name" do
city = City.new(name: nil)
city.valid?
expect(city.errors[:name]).to include("can't be blank")
end
it "is invalid without a population number" do
city = City.new(population: nil)
city.valid?
expect(city.errors[:population]).to include("can't be blank")
end
end
We can now run:
rspec
And see that our test are failing :-(. Not to worry because to pass we just need to add simple validation like for our models/country.rb.
class City < ActiveRecord::Base
belongs_to :country
validates :name, :population, presence: true
end
Refactoring models tests
Factory Girl
One of the gem that we installed along with RSpec is called factory_girl_rails and it simply creates data to test our app. When we generated our models earlier it automatically created a set of files in the factories folder.
spec
├── factories
| ├── cities.rb
| └── countries.rb
├── models
Here is how we set up spec/factories/cities.rb:
FactoryGirl.define do
factory :city do
association :country
name { Faker::Address.country }
population { Faker::Number.number(7) }
end
end
And spec/factories/countries.rb:
FactoryGirl.define do
factory :country do
name { Faker::Address.country }
population { Faker::Number.number(7) }
end
end
We'll also use Faker to generate realistic dummy data so we don't have to.
One little config trick to add to rails_helper.rb:
# Include Factory Girl syntax to simplify calls to factories
config.include FactoryGirl::Syntax::Methods
This line just simplify writing our test as we don't need to add Factory.build
or Factory.create
or FactoryGirl.attributes_for
every time we set up a data object.
And we can refactor our tests to a simpler:
require 'rails_helper'
describe Country do
describe "validations" do
it "is valid with a name and a population number" do
country = build(:country)
expect(country).to be_valid
end
it "is invalid without a name" do
country = build(:country, name: nil)
country.valid?
expect(country.errors[:name]).to include("can't be blank")
end
it "is invalid without a population number" do
country = build(:country, population: nil)
country.valid?
expect(country.errors[:population]).to include("can't be blank")
end
end
describe "associations" do
it "has many cities" do
assc = described_class.reflect_on_association(:cities)
expect(assc.macro).to eq :has_many
end
end
it "has a valid factory" do
expect(build(:country)).to be_valid
end
end
We also have added another test to verify our associations using ActiveRecord reflect_on_association.
Shoulda-matchers
Another way to refactor our models specs would be to use the shoulda-matchers gem
.
It would simplify our country_spec.rb to:
require 'rails_helper'
describe Country do
context 'associations' do
it { should have_many(:cities) }
end
context 'validations' do
it { should validate_presence_of :name }
it { should validate_presence_of :population }
end
it "has a valid factory" do
expect(build(:country)).to be_valid
end
end
You can take a look at the example app on github to see how to refactor the city_spec.rb. Again it doesn't really change much from the country model you just need to assess the presence of the ActiveRecord belongs_to
association.
describe City do
describe "validations" do
it {should belong_to(:country)}
end
...
And that's about it for our models. With the help of Factorygirl, Fakers and Shoulda-matchers we have refactored our tests to cover our models validations and associations using just a few lines of code. It's time now to move on to test our controllers.
Controllers specs
When starting with the task of testing your controller you should generate a scaffold to see how Rspec generator typically create a default test. Once your application is correctly set up with Rspec, generating models, controllers or scaffolds from the command line will also create associated specs.
We can run rails g scaffold Country name population:integer --skip
and have a look at the generated files. Just make sure to use --skip
so we are not overwriting the model models/country.rb, the associated specs specs/moodels/country_spec.rb and factories spec/factories/countries.rb as we just set them up.
The basic syntax of a controller spec:
We use a describe
block to describe each controller actions, that by convention as instance methods are prefixed with a hash symbol. We also indicate the corresponding HTTP verb. Then we use an it
block for the test case.
We will be making small changes to the default spec like including Factorygirl attributes_for
or create(:country)
to simplify it, the rest is pretty much standard.
We'll also create an :invalid_country
in our factory factories/country.rb to test when passing invalid attributes.
factory :invalid_country do
name nil
end
Here is our complete controller spec for CountriesController
.
require'rails_helper'
describe CountriesController do
describe 'GET #index' do
it "populates an array of all countries" do
france = create(:country, name: "France", population: 60000000)
germany = create(:country, name: "Germany", population: 80000000)
get :index
expect(assigns(:countries)).to match_array([france, germany])
end
it "renders the :index template" do
get :index
expect(response).to render_template :index
end
end
describe 'GET #show' do
it "assigns the requested country to @country" do
country = create(:country)
get :show, id: country
expect(assigns(:country)).to eq country
end
it "renders the :show template" do
country = create(:country)
get :show, id: country
expect(response).to render_template :show
end
end
describe 'GET #new' do
it "assigns a new country to @country" do
get :new
expect(assigns(:country)).to be_a_new(Country)
end
it "renders the :new template" do
get :new
expect(response).to render_template :new
end
end
describe 'GET #edit' do
it "assigns the requested country to @country" do
country = create(:country)
get :edit, id: country
expect(assigns(:country)).to eq country
end
it "renders the :edit template" do
country = create(:country)
get :edit, id: country
expect(response).to render_template :edit
end
end
describe 'POST #create' do
context "with valid attributes" do
it "saves the new country in the database" do
expect{
post :create, country: attributes_for(:country)
}.to change(Country, :count).by(1)
end
it "redirects to countries#show" do
post :create, country: attributes_for(:country)
expect(response).to redirect_to country_path(assigns[:country])
end
end
context "with invalid attributes" do
it "does not save the new country in the database" do
post :create, country: attributes_for(:country, name: nil)
expect(Country.count).to eq(0)
end
it "re-renders the :new template" do
post :create, country: attributes_for(:country, name: nil)
expect(response).to render_template :new
end
end
end
describe 'PATCH #update' do
before :each do
@country = create(:country, name: 'Germany', population: 80000000)
end
context "with valid attributes" do
it "locates the requested @country" do
patch :update, id: @country, country: attributes_for(:country)
expect(assigns(:country)).to eq(@country)
end
it "changes @country's attributes" do
patch :update, id: @country,
country: attributes_for(:country,
name: 'Switzerland',
population: 8000000)
@country.reload
expect(@country.name).to eq('Switzerland')
expect(@country.population).to eq(8000000)
end
it "redirects to the updated country" do
patch :update, id: @country,
country: attributes_for(:country)
expect(response).to redirect_to @country
end
end
context "with invalid attributes" do
it "does not change the country's attributes" do
patch :update, id: @country,
country: attributes_for(:country,
name: 'Swizterland',
population: nil)
@country.reload
expect(@country.name).not_to eq('Swizterland')
expect(@country.population).to eq(80000000)
end
it "re-renders the :edit template" do
patch :update, id: @country,
country: attributes_for(:country, name: nil)
expect(response).to render_template :edit
end
end
end
describe 'DELETE #destroy' do
before :each do
@country = create(:country)
end
it "deletes the country from the database" do
expect{
delete :destroy, id: @country
}.to change(Country,:count).by(-1)
end
it "redirects to countries#index" do
delete :destroy, id: @country
expect(response).to redirect_to countries_path
end
end
end
Features specs
We have now spent a good amount of time installing and configuring Rspec and created a series of unit tests for our models and controllers. It's time now to integrate features tests or as it is sometimes called acceptance tests.
This is the favorite part of testing for Rails developers as we get to test the real functionalities and rendering of the app. We will be using Capybara to simulate real-world use of the application.
capybara
So as mentioned Capybara lets you simulate how a user would interact with your application through a web browser, using a series of easy to understand methods like click_link
, fill_in
, and visit
.
Let's practice with our first test to check that our user is visiting our homepage.
In our spec folder we create a features folder with a file called user_visits_homepage_spec.rb
.
spec
├── features
| └── user_visits_homepage_spec.rb
├── models
require "rails_helper"
feature "User visits homepage" do
scenario "successfully" do
visit root_path
expect(page).to have_css 'h1'
expect(page).to have_content('Listing Countries')
end
end
Capybara uses his own DSL (Domain Specific Language) and introduce the notion of feature
and scenario
to define a test. You can also learn more about capybara helpers and methods like have_css
and have_content
in the documentation.
Let's now build an actual feature test and learn more about Capybara syntaxes. Can you guess what this feature spec does?
spec
├── features
| ├── user_creates_country_spec.rb
| └── user_visits_homepage_spec.rb
├── models
require "rails_helper"
feature "Countries management" do
scenario "User creates country" do
visit root_path
expect{
click_link "New Country"
fill_in "Name", with: "Colombia"
fill_in "Population", with: "60000000"
click_button "Create Country"
}.to change(Country, :count).by(1)
country = Country.last
expect(country).not_to be_nil
expect(current_path).to eq country_path(country)
expect(page).to have_content "Country was successfully created."
visit root_path
expect(page).to have_css 'table td', text: "Colombia"
end
end
As you may have guessed we have implemented a test user_creates_country_spec.rb
that covers the creation of a new country. It is pretty self explanatory, we are simulating a series of events and expecting to find the country created in a table in our root page.
We can run rspec
and watch the test fail and then implement the solution.
To make the test pass we'll have to assign the root
of our app to our countries#index
in our config/routes.rb file.
We can also use a similar test to cover the creation of a city user_creates_city_spec.rb, with the slight difference that when filling the form we need to select the Country where the city belongs.
require "rails_helper"
feature "Cities management" do
let!(:country) do
create(:country, name: 'Colombia', population: 60000000)
end
scenario "User creates city" do
visit root_path
expect{
click_link 'New City'
select 'Colombia', from: 'Country'
fill_in 'Name', with: 'Bogota'
fill_in 'Population', with: '60000000'
click_button 'Create City'
}.to change(City, :count).by(1)
city = City.last
expect(city).not_to be_nil
expect(current_path).to eq city_path(city)
expect(page).to have_content 'City was successfully created.'
end
end
We have introduced a new syntax let
that you will see a lot amongst projects using Rspec.
Let is one of many Rspec helpers that simplify writing test, provide best practice and reduce keystrokes.
Up until now we have used :before
blocks to assign test data to instance variables.
In this particular case we'll use let!
so that the value is persisted into the database. The one core difference to before blocks is that you get an explicit reference to this variable, rather than needing to fall back to instance variables.
Also let() is lazy-evaluated. This means that let() is not evaluated until the method that it defines is run for the first time.
To make user_creates_city_spec.rb pass we need first to add a New City
button to our countries/index.html.erb:
ruby rhtml
<%= link_to 'New City', new_city_path %>
And then we can use a scaffold rails g scaffold City name population:integer --skip
to create the controller and the views. For now we can remove the file cities_controller_spec.rb that was automaticaly created.
To complete this step we will need to add a select field to create a City for a specific country to our cities/_form.html.erb
<div class="field">
<%= f.label :country_id %>
<%= f.collection_select :country_id, Country.all, :id, :name, {include_blank: 'Choose a country'} %>
</div>
And assign the country_id
in the cities_controller_spec.rb create
method
@city.country_id = params[:city][:country_id]
Four Phase Test Pattern
After going through these rspec examples, you may recognize a pattern for writing test. For implementing testing best practice you should follow the Four-Phase Test pattern:
test do
# setup - Prepare object for the test
# exercise - Execute the functionality we are testing
# verify - Verify the exercise's result against our expectation
# teardown - Resetting all data to pre-test state
end
Testing with Javascript
We need to implement some changes in order to test our Ajax request and how we dynamically populate part of our tables. As you remember the whole purpose of this application is to fetch cities associated to a country through Ajax.
If your application or front-end rely heavily on Javascript you might need to use a specific testing framework like Mocha or Jasmine but in our case Selenium web-driver will be just fine.
Although Capybara’s default Rack::Test
driver does not support JavaScript, it bundles support for the Selenium web driver out of the box. With Selenium, you can simulate more complex web interactions, including JavaScript. Selenium makes this possible by running your test code through a lightweight web server, and automating the browser’s interactions with that server.
As you'll see when running your test it starts Firefox and run the Javascript interaction.
We also need to configure Database Cleaner to help with database transactions in our tests.
Database cleaner
Database Cleaner is a set of strategies for cleaning your database. You can find more information about use and config in the documentation. You may want to read more about the strategies
We need to make the following adjustments to spec/rails_helper.rb
to integrate the database_cleaner gem.
First switch this line to false
.
config.use_transactional_fixtures = false
And then paste the following configuration:
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
The final test
We'll create features/user_toggles_city_list_spec.rb
require "rails_helper"
feature "Cities management" do
let(:country) do
create(:country, name: 'Colombia', population: 60000000)
end
let!(:city) do
create(:city, country_id: country.id, name: 'Bogota', population: 60000000)
end
scenario "toggles city list", js: true do
visit root_path
find('a#Colombia').click
expect(page).to have_css 'table td', text: 'Bogota'
end
end
To enable selenium to run we'll add js: true
to our scenario.
To make this test green we will implement our Ajax request.
First we'll define a custom route:
get 'countries/cities/:id', to: 'countries#cities', as: 'countries_cities'
Then we create a method in our CountriesController
:
def cities
@country = Country.find(params[:id])
respond_to do |format|
format.js
end
end
As we specified format.js
this method will look for a views/countries/cities.js.erb file. Let's create it.
$('#cities_<%= @country.id %>').html("<%= j (render 'countries/cities') %>");
$('#cities_<%= @country.id %>').fadeToggle();
And we'll render a partial views/countries/_cities.html.erb.
<% @country.cities.each do |city| %>
<tr class="cities">
<td> </td>
<td><%= city.id %></td>
<td><%= city.name %></td>
<td><%= city.population %></td>
</tr>
<% end %>
We will also make some change to the table in our views/countries/index.html.erb and use the remote:
option.
<table class="table">
<thead>
<tr>
<th></th>
<th>id</th>
<th>Name</th>
<th>Population</th>
</tr>
</thead>
<% @countries.each do |country| %>
<tbody>
<tr>
<td><%= link_to '+', countries_cities_path(country), remote: true, id: country.name %></td>
<td><%= country.id %></td>
<td><%= country.name %></td>
<td><%= country.population %></td>
</tr>
</tbody>
<tbody id='cities_<%= country.id %>' style='display: none'></tbody>
<% end %>
</table>
And that's it. All our test are now passing (green) and we have build a strong coverage of our application.
Skipping controllers specs
Many experienced Rails developers would remove from their testing arsenal controllers specs altogether. Controllers specs AND feature specs will overlap and making sure that your app works in the browser with simulated user experience just makes more sense. This is where TDD (Test Driven Development) gets a bit of BDD (Behaviour Driven Development) and we extend testing to incorporate business requirements. If you work with Agile methodology and implement new features from users stories you can directly write the scenarios into your tests.
Getting an extensive test coverage is very important in our project development however if you have time only for one just focus on features and don't waste time writing controllers specs. In most cases, we don’t use controller specs unless there is complex logic inside the controller.
comments powered by Disqus