Getting Answers to
Your Testing Questions
Josh Justice
CodingItWrong
CodingItWrong
@bignerdranch
bignerdranch.com
Ruby on Rails course
June 27–July 1
bit.ly/nerdrails
How do I get started
testing? 🤔
Questions! 🤔
• Do I write the test or production code first?
• What do I test first?
• How many acceptance/unit tests do I write?
• How much test code do I write at a time? Production code?
• Do I test every line of code and configuration?
• How much do I use test doubles? What do I test for?
Everyone agrees!
🙈
"Unit tests are good!"
"Unit tests are bad!"
"Mocks are good!"
"Mocks are bad!"
😩
What if we didn’t have
to worry about all those
questions at once?
Test-Driven
Development:
An approach to testing that
provides a consistent set of
answers to those questions.
“TDD is dead”…?
Too rigid?
• Not “follow this exactly or you’re a bad developer.”
• Not “this is the only way it can be done.”
• Give it a whole-hearted try. Then you’ll know when
to apply it and when not to.
• Or, just take a principle or two and see if they help.
Goals
• Show TDD applied to a small real-world example.
• Show how it answers those questions about how to
get started in testing.
• Motivate you to try it if you haven’t (or if you
haven’t strictly).
Not Goals
• Convince you testing is a good idea.
• Introduce testing concepts and terms.
• Provide rationale for individual points of TDD.
learntdd.in/rails
Requirement: as a
user, I want to be able
to create a blog post.
(of course)
Do I write the test or
production code first?
Write tests first.
🤔
Why tests first?
• To make sure there's time to test.
• To make sure your code is covered by tests.
• To make sure your code is easy to test.
• To let tests drive your design.
What do I test first?
Start outside the
system.
🤔
How much test do I
write at a time?
Write a whole
acceptance test
for one feature.
🤔
# spec/features/creating_a_blog_post_spec.rb
require 'rails_helper'
describe 'Creating a blog post' do
it 'saves and displays the resulting blog post' do
visit '/blog_posts/new'
fill_in 'Title', with: 'Hello, World!'
fill_in 'Body', with: 'Hello, I say!'
click_on 'Create Blog Post'
blog_post = BlogPost.order("id").last
expect(blog_post.title).to eq('Hello, World!')
expect(blog_post.body).to eq('Hello, I say!')
expect(page).to have_content('Hello, World!')
expect(page).to have_content('Hello, I say!')
end
end
# spec/features/creating_a_blog_post_spec.rb
require 'rails_helper'
describe 'Creating a blog post' do
it 'saves and displays the resulting blog post' do
visit '/blog_posts/new'
fill_in 'Title', with: 'Hello, World!'
fill_in 'Body', with: 'Hello, I say!'
click_on 'Create Blog Post'
blog_post = BlogPost.order("id").last
expect(blog_post.title).to eq('Hello, World!')
expect(blog_post.body).to eq('Hello, I say!')
expect(page).to have_content('Hello, World!')
expect(page).to have_content('Hello, I say!')
end
end How much do I use test doubles?
In acceptance tests,
don’t use test doubles.
🤔
Run the test and watch
it fail, to know what to
implement first.
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: visit '/blog_posts/new'
ActionController::RoutingError:
No route matches [GET] "/blog_posts/new"
# config/routes.rb
Rails.application.routes.draw do
resources :blog_posts
end
# config/routes.rb
Rails.application.routes.draw do
resources :blog_posts
end
Do I test every line?
No, you can fix
trivial errors directly.
🤔
# config/routes.rb
Rails.application.routes.draw do
resources :blog_posts
end
How much production code
do I write at a time?
Just enough to fix
the current error.
🤔
Red-Green-Refactor
A note on refactoring.
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: visit '/blog_posts/new'
ActionController::RoutingError:
uninitialized constant BlogPostsController
# app/controllers/blog_posts_controller.rb
class BlogPostsController < ApplicationController
end
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: visit '/blog_posts/new'
AbstractController::ActionNotFound:
The action 'new' could not be found for BlogPostsController
# app/controllers/blog_posts_controller.rb
class BlogPostsController < ApplicationController
def new
end
end
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: visit '/blog_posts/new'
ActionView::MissingTemplate:
Missing template blog_posts/new
<%# app/views/blog_posts/new.html.erb %>
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: fill_in 'Title', with: 'Hello, World!'
Capybara::ElementNotFound:
Unable to find field "Title"
<%# app/views/blog_posts/new.html.erb %>
<%= form_for @blog_post do |f| %>
<div>
<%= f.label :title %>
<%= f.text_field :title %>
</div>
<div>
<%= f.label :body %>
<%= f.text_area :body %>
</div>
<%= f.submit 'Create Blog Post' %>
<% end %>
<%# app/views/blog_posts/new.html.erb %>
<%= form_for @blog_post do |f| %>
<div>
<%= f.label :title %>
<%= f.text_field :title %>
</div>
<div>
<%= f.label :body %>
<%= f.text_area :body %>
</div>
<%= f.submit 'Create Blog Post' %>
<% end %>
How much production code
do I write at a time?
Sometimes, more than enough
to fix the current error.
🤔
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: <%= form_for @blog_post do |f| %>
ActionView::Template::Error:
First argument in form cannot contain nil or be empty
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: <%= form_for @blog_post do |f| %>
ActionView::Template::Error:
First argument in form cannot contain nil or be empty
When do I write unit tests?
Step down to a unit test when
there are behavioral errors.
🤔
Why unit test when there's
already an acceptance test?
Acceptance tests
demonstrate external
quality: whether the
system works.
Acceptance tests don't
demonstrate internal
quality: whether the
code is maintainable.
Unit tests expose internal
quality. They drive design.
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
describe '#new' do
it 'returns a blog post' do
blog_post = instance_double(BlogPost)
expect(BlogPost).to receive(:new).and_return(blog_post)
get :new
expect(assigns[:blog_post]).to eq(blog_post)
end
end
end
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
describe '#new' do
it 'returns a blog post' do
blog_post = instance_double(BlogPost)
expect(BlogPost).to receive(:new).and_return(blog_post)
get :new
expect(assigns[:blog_post]).to eq(blog_post)
end
end
end
How much test do I write?
Write only enough unit test to
expose the behavioral error.
🤔
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
describe '#new' do
it 'returns a blog post' do
blog_post = instance_double(BlogPost)
expect(BlogPost).to receive(:new).and_return(blog_post)
get :new
expect(assigns[:blog_post]).to eq(blog_post)
end
end
end
How much test do I write?
Specify one behavior per
unit test case.
🤔
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
describe '#new' do
it 'returns a blog post' do
blog_post = instance_double(BlogPost)
expect(BlogPost).to receive(:new).and_return(blog_post)
get :new
expect(assigns[:blog_post]).to eq(blog_post)
end
end
end
How much do I use test doubles?
In unit tests, use test doubles
in place of any collaborators.
🤔
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
describe '#new' do
it 'returns a blog post' do
blog_post = instance_double(BlogPost)
expect(BlogPost).to receive(:new).and_return(blog_post)
get :new
expect(assigns[:blog_post]).to eq(blog_post)
end
end
end
What do I test for?
In unit tests, behavior, not
state. (Mostly.)
🤔
# rspec spec/controllers/blog_posts_controller_spec.rb
F
Failures:
1) BlogPostsController#new returns a blog post
Failure/Error: blog_post = instance_double(BlogPost)
NameError:
uninitialized constant BlogPost
# db/migrate/20160223100510_create_blog_posts.rb
class CreateBlogPosts < ActiveRecord::Migration
def change
create_table :blog_posts do |t|
t.string :title
t.text :body
end
end
end
# app/models/blog_post.rb
class BlogPost < ActiveRecord::Base
end
# rspec spec/controllers/blog_posts_controller_spec.rb
F
Failures:
1) BlogPostsController#new returns a blog post
Failure/Error: expect(assigns[:blog_post]).to eq(blog_post)
expected: #<InstanceDouble(BlogPost) (anonymous)>
got: nil
(compared using ==)
# app/controllers/blog_posts_controller.rb
class BlogPostsController < ApplicationController
def new
@blog_post = BlogPost.new
end
end
# rspec spec/controllers/blog_posts_controller_spec.rb
.
Finished in 0.03134 seconds (files took 1.46 seconds to load)
1 example, 0 failures
How often do I run which
tests?
When the unit test
passes, step back up
to the acceptance test.
🤔
Two Red-Green-
Refactor Loops
# spec/features/creating_a_blog_post_spec.rb
require 'rails_helper'
describe 'Creating a blog post' do
it 'saves and displays the resulting blog post' do
visit '/blog_posts/new'
fill_in 'Title', with: 'Hello, World!'
fill_in 'Body', with: 'Hello, I say!'
click_on 'Create Blog Post'
blog_post = BlogPost.order("id").last
expect(blog_post.title).to eq('Hello, World!')
expect(blog_post.body).to eq('Hello, I say!')
expect(page).to have_content('Hello, World!')
expect(page).to have_content('Hello, I say!')
end
end
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: click_on 'Create Blog Post'
AbstractController::ActionNotFound:
The action 'create' could not be found for
BlogPostsController
# app/controllers/blog_posts_controller.rb
class BlogPostsController < ApplicationController
def new
...
end
def create
end
end
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: click_on 'Create Blog Post'
ActionView::MissingTemplate:
Missing template blog_posts/create
<%# app/views/blog_posts/create.html.erb %>
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: expect(blog_post.title).to eq('Hello, World!')
NoMethodError:
undefined method `title' for nil:NilClass
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
describe '#new' do
...
end
describe '#create' do
it 'creates a blog post record' do
expect(BlogPost).to receive(:create).with(title: 'My Title',
body: 'My Body')
post :create, {
blog_post: {
title: 'My Title',
body: 'My Body',
}
}
end
end
end
# rspec spec/controllers/blog_posts_controller_spec.rb
.F
Failures:
1) BlogPostsController#create creates a blog post record
Failure/Error:
expect(BlogPost).to receive(:create).with(title: 'My Title',
body: 'My Body')
(BlogPost(id: integer, title: string, body: text)
(class)).create({:title=>"My Title", :body=>"My Body"})
expected: 1 time with arguments: ({:title=>"My
Title", :body=>"My Body"})
received: 0 times
# app/controllers/blog_posts_controller.rb
class BlogPostsController < ApplicationController
...
def create
BlogPost.create(params[:blog_post])
end
end
☝😧 Wait for iiiiiiiit…
# rspec spec/controllers/blog_posts_controller_spec.rb
..
Finished in 0.02774 seconds (files took 1.53 seconds to load)
2 examples, 0 failures
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: BlogPost.create(params[:blog_post])
ActiveModel::ForbiddenAttributesError
# app/controllers/blog_posts_controller.rb
class BlogPostsController < ApplicationController
...
def create
BlogPost.create(blog_post_params)
end
private
def blog_post_params
params.require(:blog_post).permit(:title, :body)
end
end
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: expect(page).to have_content('Hello, World!')
expected to find text "Hello, World!" in ""
<%# app/views/blog_posts/create.html.erb %>
<h1><%= @blog_post.title %></h1>
<div>
<%= @blog_post.body %>
</div>
# rspec spec/features/creating_a_blog_post_spec.rb
F
Failures:
1) Creating a blog post saves and displays the resulting blog
post
Failure/Error: <h1><%= @blog_post.title %></h1>
ActionView::Template::Error:
undefined method `title' for nil:NilClass
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
...
describe '#create' do
it 'creates a blog post record' do
...
end
it 'returns the new blog post to the view' do
blog_post = instance_double(BlogPost)
allow(BlogPost).to receive(:create).and_return(blog_post)
post :create, {
blog_post: {
title: 'My Title',
body: 'My Body',
}
}
expect(assigns[:blog_post]).to eq(blog_post)
end
end
end
# rspec spec/controllers/blog_posts_controller_spec.rb
..F
Failures:
1) BlogPostsController#create returns the new blog post to the
view
Failure/Error: expect(assigns[:blog_post]).to eq(blog_post)
expected: #<InstanceDouble(BlogPost) (anonymous)>
got: nil
(compared using ==)
# app/controllers/blog_posts_controller.rb
class BlogPostsController < ApplicationController
...
def create
@blog_post = BlogPost.create(blog_post_params)
end
...
end
# rspec spec/controllers/blog_posts_controller_spec.rb
...
Finished in 0.03122 seconds (files took 1.52 seconds to load)
3 examples, 0 failures
# rspec
....
Finished in 0.17293 seconds (files took 1.49 seconds to load)
4 examples, 0 failures
# rspec spec/features/creating_a_blog_post_spec.rb
.
Finished in 0.15204 seconds (files took 1.42 seconds to load)
1 example, 0 failures
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
...
describe '#create' do
it 'creates a blog post record' do
expect(BlogPost).to receive(:create).with(title: 'My Title',
body: 'My Body')
post :create, {
blog_post: {
title: 'My Title',
body: 'My Body',
}
}
end
it 'returns the new blog post to the view' do
blog_post = instance_double(BlogPost)
allow(BlogPost).to receive(:create).and_return(blog_post)
post :create, {
blog_post: {
title: 'My Title',
body: 'My Body',
}
}
expect(assigns[:blog_post]).to eq(blog_post)
end
end
end
# spec/controllers/blog_posts_controller_spec.rb
require 'rails_helper'
describe BlogPostsController do
...
describe '#create' do
let(:post_params) {
{
blog_post: {
title: 'My Title',
body: 'My Body',
}
}
}
it 'creates a blog post record' do
expect(BlogPost).to receive(:create).with(title: 'My Title',
body: 'My Body')
post :create, post_params
end
it 'returns the new blog post to the view' do
blog_post = instance_double(BlogPost)
allow(BlogPost).to receive(:create).and_return(blog_post)
post :create, post_params
expect(assigns[:blog_post]).to eq(blog_post)
end
end
end
# rspec
....
Finished in 0.17293 seconds (files took 1.49 seconds to load)
4 examples, 0 failures
Imagine if testing the way
you want to was second-
nature.
TDD can help you
get there.
…whether you end up embracing all of it or not.
🤔 Questions?
To Learn More
• learntdd.in/rails
• The RSpec Book
• Growing Object-Oriented Software, Guided By
Tests
Thanks! 🙃
@CodingItWrong
learntdd.in/rails
Getting Answers to Your Testing Questions

Getting Answers to Your Testing Questions