In this demo we will:
- Learn why testing is important
- Learn about testing "philosophies"
- Learn how RSpec makes testing "productive and fun"
including:
- Units specs (Models: "M")
- Request & Routing specs (Controllers: "C")
- View specs (Views: "V")
- System specs (Models, Views & Controllers: "MVC")
- Learn about FactoryBot and the utility of factories in testing
NOTE: You will eventually be able to watch a presentation of this material.
ALSO NOTE: I will use the words "spec" and "test" interchangeably in this demo.
- Ubuntu 20 LTS: https://www.youtube.com/watch?v=I8WhikkiiSI
- Ruby, Node and Yarn: https://www.youtube.com/watch?v=C_xhTo9bw0s
- Microsoft Visual Studio Code: https://www.youtube.com/watch?v=rizfyb1-u6Q
Let's create our Rails application and open the code in the Visual Source Code IDE.
rails new rspec_demo
code rspec_demo
Let's open the integrated terminal using Ctrl-` (backtick) and notice that this
places the prompt in the rspec_demo
directory. So we can just commit source
code as follows:
git add .
git commit -m'rails new rspec_demo'
We'll add FactoryBot via factory_bot_rails
gem and RSpec via the rspec-rails
gem by adding them and the Pry gem mentioned last month
to the :development, :test
group in the Gemfile
as shown below.
# Gemfile
...
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'factory_bot_rails' # ADD THIS LINE!
gem 'pry' # ADD THIS LINE!
gem 'rspec-rails' # AND THIS LINE!
end
...
Now let's rid ourselves of that pesky test
directory (RSpec specs will be kept
in the spec
directory):
rm -fr test
Finally, we'll install the gem code and run RSpec's install command and commit our changes:
bundle install
rails generate rspec:install
git add .
git commit -m'Remove test directory, add factory_bot_rails, pry & rspec-rails gems and run the rspec:install generator'
Let's re-use the command we used last month to generate our User
scaffold
first:
rails generate scaffold user username:string first_name:string last_name:string bio:text bicycles:integer gpa:float birth_date:date account_expiration:datetime earthling:boolean
Did you notice a difference in the output from previous demos?! Notice these lines in the output from the Rails scaffold generator:
...
invoke rspec
create spec/models/user_spec.rb
invoke factory_bot
create spec/factories/users.rb
...
invoke rspec
create spec/requests/users_spec.rb
create spec/views/users/edit.html.erb_spec.rb
create spec/views/users/index.html.erb_spec.rb
create spec/views/users/new.html.erb_spec.rb
create spec/views/users/show.html.erb_spec.rb
create spec/routing/users_routing_spec.rb
...
Notice the rspec-rails
gem provides a
generator
that replaces the normal minitest
generator and creates "spec" files grouped
in the spec
directory. Likewise, the factory_bot_rails
gem adds a generators
the creates any needed factories. More on these shortly.
As in previous demos, we'll set our root route by making the following change to
config/routes.rb
# config/routes.rb
Rails.application.routes.draw do
root 'users#index' # ADD THIS LINE!
resources :users
end
Finally, we'll migrate the database and push (commit) our code.
rails db:migrate
git add .
git commit -m'Generate a User scaffold, set the root route and migrate the database'
Testing code with code is an integral part of agile or lean software development. At it's core the goal is to ensure that all code run in a production environment is "covered" by test code that ensures as few bugs as practical exist in the application itself.
There are many types of testing depending upon the type of application/program but general consensus is that code for Web applications should be tested at three levels; unit or model tests, controller or request/routing tests and feature or system tests.
RSpec is a "Behavior Driven Development" or BDD framework for testing applications implemented in Ruby. BDD differs from the traditional "Test Driven Development" or TDD approach in that the primary focus of BDD is upon the user's perspective rather than a pure code-centric view. This shows up most prominently in the feature/system level tests.
BDD uses a "Domain Specific Language" or DSL to provide a means of expressing tests in a language recognizable to developers and non-developers alike in order to promote shared understanding of the application's behavior.
Model or Unit specs are the base level of the testing hierachy; the "M" part of "MVC."
For each Rails Model the the Rails generator generated:
# app/models/user.rb
class User < ApplicationRecord
end
The rspec-rails
gem has generated an associated Model specification:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end
And the factory_bot_rails
gem has generated an associated factory:
# spec/factories/user.rb
FactoryBot.define do
factory :user do
username { "MyString" }
first_name { "MyString" }
last_name { "MyString" }
bio { "MyText" }
bicycles { 1 }
gpa { 1.5 }
birth_date { "2020-11-17" }
account_expiration { "2020-11-17 23:58:52" }
earthling { false }
end
end
We can run the model specs with the command: rspec spec/models/user_spec.rb
and receive the following output:
*
Pending: (Failures listed here are expected and do not affect your suite\'s status)
1) User add some examples to (or delete) /Users/bbarnett/Dropbox/src/cincinnatirb/rspec_demo/spec/models/user_spec.rb
# Not yet implemented
# ./spec/models/user_spec.rb:4
Finished in 0.02488 seconds (files took 17.08 seconds to load)
1 example, 0 failures, 1 pending
This indicates that the only spec is marked as "pending" currently. So let's change that.
Let's write a simple spec to ensure our factory creates a valid User.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
it 'is valid by default' do
valid_user = FactoryBot.create(:user)
expect(valid_user.valid?).to be
end
end
Now, when we re-run the spec we get the output below:
.
Finished in 0.09659 seconds (files took 3.5 seconds to load)
1 example, 0 failures
To ensure that we're actually implementing a feature that does not already exist it is helpful to start with a failing or "red" test. Once we have a failing test we can write the code needed to make the test pass or "green." Then it's safe to improve or "refactor" the code because we have a test that will tell us if our improvement is really an improvement or breaks our application.
To demonstrate, we probably want to ensure that each User has at minimum a
username
attribute. Let's write our failing test first:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
it 'is valid by default' do
valid_user = FactoryBot.create(:user)
expect(valid_user.valid?).to be
end
context 'validations' do
it 'is invalid without a username' do
invalid_user = FactoryBot.build(:user, username: nil)
expect(invalid_user.valid?).not_to be
end
end
end
Notice that we've used a context
block to group our specs that relate to
validating our User model. When we run the specs we get our failure:
.F
Failures:
1) User validations is invalid without a username
Failure/Error: expect(invalid_user.valid?).not_to be
expected true to evaluate to false
# ./spec/models/user_spec.rb:12:in `block (3 levels) in <top (required)>'
Finished in 0.13006 seconds (files took 4.74 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:10 # User validations is invalid without a username
Perfect. A good practice to get into is to guess where you expect the failure to happen within the spec or the implementing code. Here the spec has failed because the expected value was wrong. We can fix the spec by adding the proper validation to the User model. We'll add a simple validation on the User model to do so:
# app/models/user.rb
class User < ApplicationRecord
validates :username, presence: true
end
Running the specs again we see that we're back to green:
..
Finished in 0.06693 seconds (files took 2.5 seconds to load)
2 examples, 0 failures
Let's add one more validation spec to ensure that usernames are unique:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
let(:valid_user) { FactoryBot.create(:user) }
it 'is valid by default' do
expect(valid_user.valid?).to be
end
context 'validations' do
it 'is invalid without a username' do
invalid_user = FactoryBot.build(:user, username: nil)
expect(invalid_user.valid?).not_to be
end
it 'does not permit duplicate usernames' do
invalid_user = FactoryBot.build(:user, username: valid_user.username)
expect(invalid_user.valid?).not_to be
end
end
end
Running that we get the expected failure:
..F
Failures:
1) User validations does not permit duplicate usernames
Failure/Error: expect(invalid_user.valid?).not_to be
expected true to evaluate to false
# ./spec/models/user_spec.rb:18:in `block (3 levels) in <top (required)>'
Finished in 0.08105 seconds (files took 3.55 seconds to load)
3 examples, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:16 # User validations does not permit duplicate usernames
We'll fix the test by adding the validation to the User model:
class User < ApplicationRecord
validates :username, presence: true, uniqueness: true
end
This time, we'll run the specs with the -fd
flag to see a "human parseable"
output format: rspec spec/models/user_spec.rb -fd
User
is valid by default
validations
is invalid without a username
does not permit duplicate usernames
Finished in 0.06486 seconds (files took 3.19 seconds to load)
3 examples, 0 failures
Leveraging the features of a BDD testing framework like RSpec makes producing test output like this easy and helps non-developers understand exactly what the application's capabilities.
Refactoring applies to test code just like it does implementation code. As best
practice for factories is that they generate a minimally valid object meaning
that attributes that are not required are set to nil
by default. Thus the
factory_bot_rails
generated User factory can by trimmed to the following:
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:username) { |n| "user-#{n}" }
end
end
Here we're using the sequence
feature of FactoryBot to generate unique
usernames that conform to a specific pattern.
WARNING: Always perform refactorings from a "green" test suite whether you're refactoring test code or implementation code.
This seems like a good place to commit our code.
git add .
git commit -m'Flesh out the User model spec and trim down the User factory'
Request and Routing specs exercise the controller functionality, the "C" in "MVC."
Since Rails 5 "traditional" controller specs have fallen out of favor, in favor of Request & Routing specs. Details of this can be found in the RSpec 3.5 release announcement and are beyond the scope of this demo. We'll proceed along the lines of the current trend.
Let's just run those rspec-rails
generated Request specs and...
$ rspec spec/requests/users_spec.rb
**.**********
Pending: (Failures listed here are expected and do not affect your suite/'s status)
1) /users GET /index renders a successful response
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:27
2) /users GET /show renders a successful response
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:35
3) /users GET /edit render a successful response
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:50
...
12) /users DELETE /destroy redirects to the users list
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:123
Finished in 13.09 seconds (files took 3.58 seconds to load)
13 examples, 0 failures, 12 pending
Well that's not very helpful! Let's address the first failure.
# spec/requests/users_spec.rb
require 'rails_helper'
RSpec.describe "/users", type: :request do
let(:valid_attributes) {
{ username: 'lester' }
}
let(:invalid_attributes) {
{ username: nil }
}
...
end
Notice that removed all the "boilerplate" comments that the rspec-rails
generator placed at the top of the file and fixed the :valid_attributes
and
:invalid_attributes
let
statements. Now we'll rerun the specs using
rspec spec/requests/users_spec.rb
and see what effect that had:
........**...
Pending: (Failures listed here are expected and do not affect your suite's status)
1) /users PATCH /update with valid parameters updates the requested user
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:77
2) /users PATCH /update with valid parameters redirects to the user
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:84
Finished in 0.93628 seconds (files took 3.09 seconds to load)
13 examples, 0 failures, 2 pending
Well that's a whole lot better! We can fix the first failure like with did the previous failures by adding some new attributes to our User and then add the expectation that verifies the new attributes were persisted.
# spec/requests/users_spec.rb
...
describe "PATCH /update" do
context "with valid parameters" do
let(:new_attributes) {
{ first_name: 'Lester' }
}
it "updates the requested user" do
user = User.create! valid_attributes
patch user_url(user), params: { user: new_attributes }
user.reload
expect(user.first_name).to eq('Lester')
end
it "redirects to the user" do
user = User.create! valid_attributes
patch user_url(user), params: { user: new_attributes }
user.reload
expect(response).to redirect_to(user_url(user))
end
end
...
Running the specs with the -fd
flag again we see we're all green. WooT!
/users
GET /index
renders a successful response
GET /show
renders a successful response
GET /new
renders a successful response
GET /edit
render a successful response
POST /create
with valid parameters
creates a new User
redirects to the created user
with invalid parameters
does not create a new User
renders a successful response (i.e. to display the 'new' template)
PATCH /update
with valid parameters
updates the requested user
redirects to the user
with invalid parameters
renders a successful response (i.e. to display the 'edit' template)
DELETE /destroy
destroys the requested user
redirects to the users list
Finished in 1.44 seconds (files took 1.82 seconds to load)
13 examples, 0 failures
How about a commit?!
git add .
git commit -m'Flesh out User request spec'
Phew! That was a lot, and now we have to look at the Routing specs by running
rspec spec/routing/users_routing_spec.rb
:
........
Finished in 0.03222 seconds (files took 2.08 seconds to load)
8 examples, 0 failures
Well, that was easy! Moving right along...
OK, not so fast. Poke around inside the spec/routing/users_routing_spec.rb
file and see if the contents make sense to you. Hint: They should but feel to
review Tim Mecklem's excellent July 2020 CincyRB meeting video
on "MVC and Routing."
System specs test how the models, views and controllers (MVC) work together to implemented the desired application functionality.
Among the improvements introduced in Rails 5.1 was a new type of test called the System test which improved the integration test process over "traditional" Feature tests, especially those that tested features which relied on JavaScript. More details are contained in the RSpec 3.7 release announcement.
Due to the complex nature of System specs they cannot be auto-generated as with the previously seen test types. So we'll have to start with a bit of old fashioned code writing!
First we'll create the spec/system
directory and create an empty
user_management_spec.rb
file in the new directory:
mkdir spec/system
touch spec/system/user_management_spec.rb
Next we'll create a simple spec to ensure that the application can create a User.
# spec/system/user_management_spec.rb
require 'rails_helper'
RSpec.describe 'User management', type: :system do
it 'permits creation of a User' do
visit '/users/new'
fill_in 'Username', with: 'Lester'
click_button 'Create User'
expect(page).to have_text('User was successfully created.')
end
end
Just as we used a context
to group the validation specs in our User model
specs we can use a context
to group our failures cases for the User management
system spec.
# spec/system/user_management_spec.rb
require 'rails_helper'
RSpec.describe 'User management', type: :system do
it 'permits creation of a User' do
visit '/users/new'
fill_in 'Username', with: 'Lester'
click_button 'Create User'
expect(page).to have_text('User was successfully created.')
end
context 'creation failures' do
it 'displays an appropriate error message' do
visit '/users/new'
click_button 'Create User'
expect(page).to have_text("Username can't be blank")
end
end
end
Finally, let's run the entire test suite by running the following command:
rspec -fd
Oops! We broke a View spec when we added the requirement that User#username be a unique value. So we'll have to fix the failing spec.
# spec/views/users/index.html.erb_spec.rb
require 'rails_helper'
RSpec.describe "users/index", type: :view do
before(:each) do
assign(:users, [
User.create!(
username: "Username1",
first_name: "First Name",
last_name: "Last Name",
bio: "MyText",
bicycles: 2,
gpa: 3.5,
earthling: false
),
User.create!(
username: "Username2",
first_name: "First Name",
last_name: "Last Name",
bio: "MyText",
bicycles: 2,
gpa: 3.5,
earthling: false
)
])
end
it "renders a list of users" do
render
assert_select "tr>td", text: "Username1".to_s, count: 1
assert_select "tr>td", text: "Username2".to_s, count: 1
assert_select "tr>td", text: "First Name".to_s, count: 2
assert_select "tr>td", text: "Last Name".to_s, count: 2
assert_select "tr>td", text: "MyText".to_s, count: 2
assert_select "tr>td", text: 2.to_s, count: 2
assert_select "tr>td", text: 3.5.to_s, count: 2
assert_select "tr>td", text: false.to_s, count: 2
end
end
Run the full suite one more time and voilà!
User
is valid by default
validations
is invalid without a username
does not permit duplicate usernames
/users
GET /index
renders a successful response
GET /show
renders a successful response
GET /new
renders a successful response
GET /edit
render a successful response
POST /create
with valid parameters
creates a new User
redirects to the created user
with invalid parameters
does not create a new User
renders a successful response (i.e. to display the 'new' template)
PATCH /update
with valid parameters
updates the requested user
redirects to the user
with invalid parameters
renders a successful response (i.e. to display the 'edit' template)
DELETE /destroy
destroys the requested user
redirects to the users list
UsersController
routing
routes to #index
routes to #new
routes to #show
routes to #edit
routes to #create
routes to #update via PUT
routes to #update via PATCH
routes to #destroy
User management
permits creation of a User
creation failures
displays an appropriate error message
users/edit
renders the edit user form
users/index
renders a list of users
users/new
renders new user form
users/show
renders attributes in <p>
Finished in 8.64 seconds (files took 3.69 seconds to load)
33 examples, 0 failures
Let's commit!
git add .
git commit -m'Add an initial system spec and fix the users#index view spec'
## Further Reading:
* [Behavior Driven Development](https://en.wikipedia.org/wiki/Behavior-driven_development) on Wikipedia
* [Domain Specific Language](https://en.wikipedia.org/wiki/Domain-specific_language) on Wikipedia
* [MVC & Routing](https://www.youtube.com/watch?v=XRwGB0TpB1g) presentation from July 2020 CincyRB meeting