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