Skip to content

Instantly share code, notes, and snippets.

@agilous
Last active January 21, 2021 05:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save agilous/f6773428d6dfd192cea9aefb00ccdf03 to your computer and use it in GitHub Desktop.
Save agilous/f6773428d6dfd192cea9aefb00ccdf03 to your computer and use it in GitHub Desktop.
RSpec Demo

RSpec Demo

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.

1. Prerequisites

2. Starting from rails new

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'

3. Installing the FactoryBot & RSpec Gems

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'

4. Generating a User

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'

5. A Word about Testing

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.

6. Model/Unit Specs & Factories

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

7. Red. Green. Refactor.

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'

8. Controll..., eh..., Request & Routing Specs

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."

9. System Specs

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment