| z, ? | toggle help (this) |
| space, → | next slide |
| shift-space, ← | previous slide |
| d | toggle debug mode |
| ## <ret> | go to slide # |
| c, t | table of contents (vi) |
| f | toggle footer |
| r | reload slides |
| n | toggle notes |
| p | run preshow |
subject?let or let!?Next: DSL or no DSL
# RSpec
describe User do
it "should be valid" do
FactoryGirl.build(:user).should be_valid
end
end
# versus
# TestUnit
class UserTest < Test::Unit::Case
def test_user_validity
assert FactoryGirl.build(:user).valid?
end
end
Next: Better rspec example
# RSpec
describe User do
it "should be valid" do
FactoryGirl.build(:user).should be_valid
end
end
# versus
# RSpec
describe User do
subject { FactoryGirl.build(:user) }
it { should be_valid }
end
Next: event better espec example
# RSpec
describe User, "a new record when login is set to 'anakin'" do
let(:login) { 'anakin' }
subject { Factory.build(:user, :login => login) }
it { should be_valid } # subject.valid?
it { should_not be_persisted } # subject.persisted?
its(:login) { should == 'anakin' }
context "when login is set to 'vader'" do
let(:login) { 'vader' }
its(:login) { should == 'vader' }
end
# ... an so on
end
Next: tests output
.....
--format documentation option)User
a new record when login is set to 'anakin'
should be valid
it should not be persisted
login
should == 'anakin'
when login is set to 'vader'
login
should == 'vader'
Next: Good practices (clean specs)
Admin::ProductsController
on GET to :show
should find the product
should assign @product
should respond with 200
should render template :show
on GET to :new
should create a new product instance
should assign @product
should respond with 200
should render template :new
on POST to :create
should create a new product instance
should assign @product
should try to save a product
with valid attributes
should set the flash
should respond with 300..399
should redirect to "/admin/products/1444"
with invalid attributes
should not set the flash
should respond with 200
should render template :new
Subscription
factories
should have :subscription factory
should have :active_subscription factory
should have :cancelled_subscription factory
db table
should have db column named user_id of type integer of null false
should have db column named product_id of type integer of null false
should have db column named order_number of type string of null false
should have a index on columns user_id
should have a index on columns product_id
should have a index on columns status
associations
should belong to user
should belong to product
should have many payments
validations
Subscription::SubscriptionUniquenessValidator
when user has no subscriptions
should allow to create #active subscription
should allow to create #cancelled subscription
when user has several #cancelled subscriptions for the same product
should allow to create #active subscription for this product
should allow to create #cancelled subscription for this product
when user has an #active subscription for the given product
should not allow to create #active subscription for this product
should allow to create #cancelled subscription for this product
should not allow to change other product's subscription status to #active
should allow to change #active subscription status
should allow to update #active subscription
should allow to create #active subscription for the other product
Next: describe what you are doing
describe User do
describe '.authenticate' do
end
describe '.admins' do
end
describe '#admin?' do
end
describe '#name' do
end
endclass SessionsController < ApplicationController
def create
user = User.authenticate(params)
if user.present?
# do something
else
# do something different
end
end
end
describe SessionsController do
describe '#create' do
context 'on success'
context 'on failure'
end
end
Next: spec for model
describe User do
subject { Factory(:user) }
describe "database table" do
it { should have_db_column('email') }
end
describe "associations" do
it { should have_many(:posts) }
end
describe "validations" do
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
end
describe "scopes" do
describe ".bloked"
describe ".paginated"
end
describe "#full_name"
end
Next: use the right matcher
specify { user.valid?.should == true }
# generates the following specdoc
'User should == true' FAILED
expected: true,
got: false (using ==)
subject { user }
it { should be_valid }
# generates the following specdoc
'User should be valid' FAILED
expected valid? to return true, got false
Next: should matchers
Next: shoulda example
# the model
class User < ActiveRecord::Base
belongs_to :account
has_many :posts
validates_presence_of :email
allows_values_for :email, "test@example.com"
end
# ..and the corresponding spec
describe User do
it { should belong_to(:account) }
it { should have_many(:posts) }
it { should validate_presence_of(:email) }
it { should allow_value("test@example.com").for(:email) }
end
Next: it expects one thing
describe UsersController, '#create' do
# setup spec...
it 'creates a new user' do
should assign_to(:user).with(user)
should set_the_flash
should respond_with(:redirect)
should redirect_to(admin_user_path(user))
end
end
# vs.
describe UsersController, '#create' do
# setup spec...
it { should assign_to(:user).with(user) }
it { should set_the_flash }
it { should respond_with(:redirect) }
it { should redirect_to(admin_user_path(user)) }
endspecify, it and subjectit "should be valid" do
@user.should be_valid
end
# versus
it { @user.should be_valid }
# versus
subject { @user }
it { should be_valid }
Next: use let and let!
let and let!# ...let there be let!
describe BlogPost do
let(:blog_post) do
FactoryGirl.create(:post, :title => 'Hello')
end
it "does something" do
blog_post.should ...
end
it "does something else" do
blog_post.should ...
end
end
Next: tricks with let
let(:product) { mock_model(Product) }
describe "on POST to :create" do
before do
Product.stub(:new).and_return(product)
product.stub(:save).and_return(success)
post :create, :product => { 'name' => 'Test' }
end
context "on success" do
let(:success) { true }
it { should set_the_flash }
it { should respond_with(:redirect) }
it { should redirect_to(admin_product_path(product)) }
end
context "on failure" do
let(:success) { false }
it { should_not set_the_flash }
it { should respond_with(:success) }
it { should render_template(:new) }
end
end
Next: shared context
class Subscription < ActiveRecord::Base
STATUS_ACTIVE = 'active'
STATUS_CANCELLED = 'cancelled'
def self.with_status(*statuses)
where("status IN (?)", statuses)
end
enddescribe Subscription, "#with_status" do
let!(:active) { FactoryGirl.create(:active_subscription) }
let!(:cancelled) { FactoryGirl.create(:cancelled_subscription) }
# define shared context
def self.when_status(*statuses, &ctx_block)
ctx = context "when status is #{statuses.join('or ')}" do
let(:result) { Subscription.with_status(*statuses) }
end
ctx.class_eval(&ctx_block)
end
when_status(Subscription::STATUS_ACTIVE) do
specify { result.should have(1).item }
specify { result.should include(active) }
end
when_status(Subscription::STATUS_ACTIVE,
Subscription::STATUS_CANCELLED) do
specify { result.should have(2).items }
specify { result.should include(active) }
specify { result.should include(cancelled) }
end
end
Next: good practices (fast specs)
Next: fast specs for controllers
let(:user) { mock_model(User) }
describe "on GET to :index" do
before do
User.stub(:all).and_return([])
get :index
end
it { should respond_with(:success) }
it { should_not set_the_flash }
it { should render_template(:index) }
it { should assign_to(:users) }
end
Next: mockmodel vs. stubmodel
stub_model - creates an instance of Model
common ActiveModel methods are stubbed out:
mock_model | stub_mode | source
Next: stubbing pain
def index
@followers_tweets = current_user.followers_tweets.limit(20)
@recent_tweet = current_user.tweets.first
@following = current_user.following.limit(5)
@followers = current_user.followers.limit(5)
@recent_favorite = current_user.favorite_tweets.first
@recent_listed = current_user.recently_listed.limit(5)
if current_user.trend_option == "worldwide"
@trends = Trend.worldwide.by_promoted.limit(10)
else
@trends = Trend.filter_by(current_user.trend_option).limit(10)
end
end
def index
@presenter = Tweets::IndexPresenter.new(current_user)
end
describe "on GET to :index" do
let(:presenter) { mock('IndexPresenter') }
before { Tweets::IndexPresenter.stubs(:new => presenter) }
# etc.
enddef show
@user = User.find(params[:id])
@subscriptions = @user.subscriptions_paginated(params[:page])
end
describe UsersController, "show" do
let(:user) { mock_model(User, :subscriptions_paginated => []) }
before do
User.stub(:find => user)
get :index, :id => user
end
# etc.
end
Making ActiveRecord Models Thin
A Paperclip Refactoring Tale: Part One: Dependency Injection