Add rspec-rails
to your Gemfile
:
group :test, :development do
gem 'sqlite3'
gem "rspec-rails", "~> 3.0"
gem "byebug"
gem 'web-console', '~> 2.0'
end
Run bundle install
This will add Rspec and RSpec Rails to our Rails application. RSpec is a commonly used TDD (and BDD) testing tool which provides a special testing language (powered by Ruby) for testing existing code and for informing developers about the structure and functionality of yet to be written code!
To complete the installation run:
rails generate rspec:install
rails generate rspec:model post
rails g rspec:model comment
rails g
is the short version of this command, and can be used anytime you
would use rails generate
Normally if you generate a Rails entity like a controller or model then this will automatically create a spec for you, but since we've already got a bunch of code that isn't tested we have to manually generate a spec to test our comment model.
We can now run the specs we've generated. Run rake spec
. Alternatively use
the rspec
command. You can run individual specs as follows:
rspec spec/models/post_spec.rb
Or all the model specs with: rspec spec/models
. With this last example you're
providing a directory for rspec
to run, it simply runs every spec it can find
in that folder and all sub-folders.
Our Post model seems fairly empty but there is already some business logic in
there that we can test. Rails validations are considered business logic and are
easy to test. Open spec/models/post_spec.rb
and update it so that it looks
like:
require 'rails_helper'
RSpec.describe Post do
it 'should validate presence of title' do
post = Post.new
post.valid?
expect(post.errors.messages[:title]).to include "can't be blank"
end
end
Once you've saved it, run rspec spec/models/post_spec.rb
. The test should
pass with 1 example, 0 failures
. But we're not done yet. Over the lifetime of
our application we'll probably be adding lots of extra functionality and our
spec is very flat. We should organise it a litte better and structure it in
such a way which also lets us reuse code:
require 'rails_helper'
RSpec.describe Post do
describe 'validations' do
subject(:post) { Post.new } # sets the subject of this describe block
before { post.valid? } # runs a precondition for the test/s
[:title, :body].each do |attribute|
it "should validate presence of #{attribute}" do
expect(post.errors[attribute].size).to be >= 1
expect(post.errors.messages[attribute]).to include "can't be blank"
end
end
end
end
What we're doing here is splitting up the test into a "validations" section and then declaring the subject of the test. The before block will invoke some common code to be run for each test and then each test just checks that there is an error on the model, and that the error is the expected error.
RSpec is a tool that provides a nice Domain Specific Language (DSL) to write specs. The important documentation to read is for expectations and matchers, but for the purposes of this project we'll be providing and explaining most of the test code for you.
Since we're writing a fully functional spec for code that is already written,
we'll need to make sure our test actually works by intentionally
"breaking" some of our code. Open app/models/post.rb
and comment out
Line 6 so your Post model looks like:
class Post < ActiveRecord::Base
has_many :comments
# validates_presence_of :body, :title
end
Once you've saved the model, you can rerun the spec with rspec spec/models/post_spec.rb
. You should receive 2 examples, 2 failures
and the
error messages will be printed in your terminal. Be careful to read the error
messages closely. When you're done uncomment Line 6 in app/models/post.rb
save the file and rerun the spec to ensure that everything is passing
correctly.
We'll also need to write a unit test for our comment model. Open
spec/models/comment_spec.rb
and update it to ensure that a comment will
always belong to a post, and the comment will always have a body. The code to
do this looks like:
require 'rails_helper'
RSpec.describe Comment do
describe 'validations' do
subject(:comment) { Comment.new }
before { comment.valid? }
[:post, :body].each do |attribute|
it "should validate presence of #{attribute}" do
expect(comment.errors[attribute].size).to be >= 1
expect(comment.errors.messages[attribute]).to include "can't be blank"
end
end
end
end
When you run rspec spec/models/comment_spec.rb
you'll receive:
Failures:
1) Comment validations should validate presence of post
Failure/Error: expect(comment.errors[attribute].size).to be >= 1
expected at least 1 error on :post, got 0
# ./spec/models/comment_spec.rb:10:in `block (4 levels) in <top (required)>'
2) Comment validations should validate presence of body
Failure/Error: expect(comment.errors[attribute].size).to be >= 1
expected at least 1 error on :body, got 0
# ./spec/models/comment_spec.rb:10:in `block (4 levels) in <top (required)>'
If you open app/models/comment.rb
you'll notice that there isn't any
validations on our comment model. If you add validates_presence_of :post, :body
into the class and re-run your spec you'll see the test pass
and "go green". Congratulations, you've just TDD'd your first piece
of application logic. One of the amazing things about working with
Rails there's a very quick feedback loop between writing a failing
test, making it pass and then suddenly having a full functional
feature in your application.
We're not done with our tests though.
Rails fully supports the concept of an acceptance test, which is a full-stack automated test that behaves exactly like someone opening a browser and clicking around your site. We'll be using Capybara primarily with RackTest.
As you probably expect by now we'll be adding a new gem to our Gemfile
. Open
the Gemfile
and add the following lines to the bottom:
group :test do
gem 'capybara'
end
Then run bundle install
to install the new gems. We only
need Capybara for our tests, so we're creating a section specifically to ensure
that Capybara is only loaded when we run our tests.
Next open spec/rails_helper.rb
(which was created when you installed spec) and
add the following lines below require 'rspec/rails'
require 'capybara/rails'
require 'capybara/rspec'
This file is full of configuration for our test suite, most of which we can ignore. If you have any problems just ask a mentor.
Next we'll create our first acceptance test.
Create a folder: spec/features
then create a file
spec/features/reading_blog_spec.rb
with the following contents:
require 'rails_helper'
feature 'Reading the Blog' do
let!(:post) { Post.create!(title: 'Awesome Blog Post', body: 'Lorem ipsum dolor sit amet') }
before do
Post.create!(title: 'Another Awesome Post', body: 'Lorem ipsum dolor sit amet')
end
scenario 'Reading the blog index' do
visit root_path
expect(page).to have_content 'Awesome Blog Post'
expect(page).to have_content 'Another Awesome Post'
end
scenario 'Reading an individual blog' do
visit root_path
click_link 'Awesome Blog Post'
expect(current_path).to eq post_path(post)
end
end
Save this file and run it with: rspec spec/features
. You should get 2 examples, 0 failures
. But once again there's a problem. We've written a test
which passes immediately, we should make it fail to check that the test is
actually checking what we want it to check. We'll open
app/views/posts/_post.html.erb
and alter the Post titles as follows:
<h2><%= link_to_unless_current 'Blog Post', post %></h2>
<%= simple_format post.body %>
Now when you rerun the spec (rspec spec/features
) you'll receive two errors:
Failure/Error: expect(page).to have_content 'Awesome Blog Post'
expected to find text "Awesome Blog Post"
Failure/Error: click_link 'Awesome Blog Post'
Capybara::ElementNotFound:
Unable to find link "Awesome Blog Post"
Undo your changes to app/views/posts/_post.html.erb
so that it once again
looks like:
<h2><%= link_to_unless_current post.title, post %></h2>
<%= simple_format post.body %>
And rerun your spec to make sure everything is okay.
Acceptance tests are really powerful since they are a high level description of how your application should function. By writing acceptance tests it's entirely possible to build an entire user-facing feature without opening your web-browser! But more importantly, it gives you a high level of confidence that a feature will work, and won't inadvertantly break if you make changes elsewhere.
We're going to make more acceptance tests now.
Create a file: spec/features/post_comments_spec.rb
with the contents:
require 'rails_helper'
feature 'Posting Comments' do
background do
@post = Post.create(title: 'Awesome Blog Post', body: 'Lorem ipsum dolor sit amet')
end
# Note this scenario doesn't test the AJAX comment posting.
scenario 'Posting a comment' do
visit post_path(@post)
comment = 'This post is just filler text. Ripped off!'
fill_in 'comment_body', with: comment
click_button 'Add comment'
expect(page).to have_content comment
end
end
Save that, then create a file: spec/features/managing_posts_spec.rb
with the
contents:
require 'rails_helper'
feature 'Managing blog posts' do
scenario 'Guests cannot create posts' do
visit root_path
click_link 'New Post'
expect(page).to have_content 'Access denied'
end
scenario 'Posting a new blog' do
visit root_path
page.driver.browser.authorize 'admin', 'secret'
click_link 'New Post'
expect(page).to have_content 'New Post'
fill_in 'Title', with: 'I love cheese'
fill_in 'Body', with: "It's pretty amazing, don't you think?"
click_button 'Create Post'
expect(page).to have_content 'I love cheese'
end
context 'with an existing blog post' do
background do
@post = Post.create(title: 'Awesome Blog Post', body: 'Lorem ipsum dolor sit amet')
end
scenario 'Editing an existing blog' do
visit post_path(@post)
page.driver.browser.authorize 'admin', 'secret'
click_link 'Edit'
fill_in 'Title', with: 'Not really Awesome Blog Post'
click_button 'Update Post'
expect(page).to have_content 'Not really Awesome Blog Post'
end
end
end
Save that file. Now let's run all of our acceptance/feature specs. Open your
terminal and run: rspec spec/features
. If everything has worked you'll get
something like:
......
Finished in 0.26705 seconds
6 examples, 0 failures
We've now tested our application with some unit tests of our models, and using acceptance tests. We've increased our confidence in our existing code and we're ready to move on to adding extra features to our blog. Before we do that though we should run all our tests and commit our code.
To run all your tests either run rake spec
or rspec
. Rake is a utility
command which provides a common interface for interacting with your
application. the spec
task simply runs rspec
internally.
git add .
git commit -m "Adding rspec and tests for existing functionality"
If we have time we'll add in an Administration panel and convert our blog posts to Markdown format. https://gist.github.com/lengarvey/ca68a7bca8bef26c6cd7