Why?
Lifted from Jay Fields' Working Effectively with Unit Tests
Code doesn't lie.
(But test descriptions still can.)
"There is a level of design abstraction where it is almost impossible to safely make any change unless the code has tests. Tests are your record of the interface of every abstraction and as such they are the wall at your back." – POODR
BTW, this was stolen straight from Sandi Metz.
No side effects
Result returned
Side effects
No result returned
Query
Command
Incoming
Outgoing
To Self
result
direct & public side effects
class Pdfer
attr_reader :invoice
def initialize(invoice)
@invoice = invoice
end
def generate
pdf_as_string = generator.call
cacher.put(pdf_as_string)
invoice.cached_in_s3 = true
pdf_as_string
end
def cacher
@cacher ||= S3Cacher.new(filename)
end
def generator
@generator ||= Generator.new(invoice)
end
def filename
"invoice-#{ invoice.token }.pdf"
end
end
def filename
"invoice-#{ invoice.token }.pdf"
end
describe Pdfable do
let(:invoice) { double('Invoice') }
subject { described_class.new(invoice) }
describe '#filename' do
it 'creates an invoice filename with the invoice token' do
expect(invoice).to receive(:token) { 'a_token' }
expect(subject.filename).to eq 'invoice-a_token.pdf'
end
end
end
describe '#filename' do
let(:invoice) { instance_double(Invoice, token: 'a_token') }
its(:filename) { should eq 'invoice-a_token.pdf' }
end
def cacher
@cacher ||= S3Cacher.new(filename)
end
def generator
@generator ||= Generator.new(invoice)
end
describe '#cacher' do
it 'instantiates, caches, and returns an S3Cacher' do
expect(subject).to receive(:filename) { 'filename' }
expect(S3Cacher).to receive(:new).with('filename').once { :an_s3_cacher }
expect(subject.cacher).to be :an_s3_cacher
expect(subject.cacher).to be :an_s3_cacher
end
end
describe '#generator' do
it 'instantiates, caches, and returns a Generator' do
expect(Generator).to receive(:new).with(invoice).once { :a_generator }
expect(subject.generator).to be :a_generator
expect(subject.generator).to be :a_generator
end
end
def generate
pdf_as_string = generator.call
cacher.put(pdf_as_string)
invoice.cached_in_s3 = true
pdf_as_string
end
describe '#generate' do
let(:generator) { instance_double(Generator) }
let(:cacher) { instance_double(S3Cacher) }
let(:invoice) { instance_double(Invoice) }
before do
allow(subject).to receive(:cacher) { cacher }
allow(subject).to receive(:generator) { generator }
end
it 'generates pdf, caches it, sets bool on invoice, and returns pdf' do
expect(generator).to receive(:call) { 'pdf string' }
expect(cacher).to receive(:put).with('pdf string')
expect(invoice).to receive(:cached_in_s3=).with(true)
expect(subject.generate).to eq 'pdf string'
end
end
class Pdfer
attr_reader :invoice, :generator, :cacher
def initialize(invoice, generator: nil, cacher: nil)
@invoice = invoice
@generator = generator || Generator.new(invoice)
@cacher = cacher || S3Cacher.new(filename)
end
def generate!
pdf = generator_pdf
cached_pdf(pdf)
pdf
end
def generate_pdf
generator.call
end
def cache_pdf(pdf_as_string)
cacher.put(pdf_as_string)
invoice.cached_in_s3 = true
end
def filename
"invoice-#{ invoice.token }.pdf"
end
end
describe Pdfer do
let(:invoice) { double(Invoice) }
let(:generator) { double(Generator) }
let(:cacher) { double(S3Cacher) }
subject { described_class.new(invoice, generator: generator, cacher: cacher) }
# ...
end
Isolates the unit under test
attr_reader :invoice, :generator, :cacher
def initialize(invoice, generator: nil, cacher: nil)
@invoice = invoice
@generator = generator || Generator.new(invoice)
@cacher = cacher || S3Cacher.new(filename)
end
describe '.new and attr_readers' do
let(:invoice) { instance_double(Invoice, token: 'token') }
subject { described_class.new(invoice) }
before do
expect(Generator).to receive(:new).with(invoice) { :a_generator }
expect(S3Cacher).to receive(:new).with('invoice-token.pdf') { :a_cacher }
end
its(:invoice) { should be invoice }
its(:generator) { should be :a_generator }
its(:cacher) { should be :a_cacher }
end
def generate!
pdf = generator_pdf
cached_pdf(pdf)
pdf
end
describe '#generate!' do
it 'generates, caches, and returns pdf' do
expect(subject).to receive(:generate_pdf) { :a_pdf }
expect(subject).to receive(:cache_pdf).with(:a_pdf)
expect(subject.generate!).to be :a_pdf
end
end
def generate_pdf
generator.call
end
describe '#generate_pdf' do
it 'calls the generator and returns its result' do
expect(generator).to receive(:call) { :generator_result }
expect(subject.generate_pdf).to be :generator_result
end
end
def cache_pdf(pdf_as_string)
cacher.put(pdf_as_string)
invoice.cached_in_s3 = true
end
describe '#cache_pdf' do
it 'caches given pdf and sets invoice cache boolean' do
expect(cacher).to receive(:put).with(:given_pdf)
expect(invoice).to receive(:cached_in_s3=).with(true)
subject.cache_pdf(:given_pdf)
end
end
describe '#cache_pdf' do
let(:invoice) { Invoice.new }
it 'caches given pdf and sets invoice cache boolean' do
expect(cacher).to receive(:put).with(:given_pdf)
expect{ subject.cache_pdf(:given_pdf) }
.to change{ invoice.cached_in_s3 }.to(true)
end
end
describe '#generate!' do
it 'generates, caches, and returns pdf' do
expect(subject).to receive(:generate_pdf) { :a_pdf }
expect(subject).to receive(:cache_pdf).with(:a_pdf)
expect(subject.generate!).to be :a_pdf
end
end
super light-weight
same object
let(:invoice) { double(Invoice) }
let(:shopper) { double('Shopper').as_null_object } # returns itself
let(:account) { double('an authenticated account', id: '123') }
light-weight
named
can be null-object
class Invoice
def call(args) end
end
describe Invoice do
let(:invoice) { instance_double(Invoice) }
it 'can be called' do
allow(invoice).to receive(:call)
subject.call
end
end
# Failures:
#
# 1) Invoice can be called
# Failure/Error: def call(args)
# ArgumentError:
# wrong number of arguments (0 for 1)
class SimpleSerializer < ActiveModel::Serializer
attributes :id
end
SimpleSerializer.new(job_object)
SimpleSerializer.new(business_object)
SimpleSerializer.new(product_object)
class DummySimpleSerializable
include ActiveModel::SerializerSupport
attr_reader :id
def initialize(id=nil)
@id = id
end
end
describe SimpleSerializer do
let(:serializable) { DummySimpleSerializable.new('123') }
subject { described_class.new(serializable) }
its(:'attributes.keys') { should contain_exactly(:id) }
end
RSpec.shared_examples 'an UnnamedSimpleSerializable' do
it { should respond_to :id }
end
describe DummySimpleSerializable do
it_behaves_like 'an UnnamedSimpleSerializable'
end
describe Job do
it_behaves_like 'an UnnamedSimpleSerializable'
# ...
end
describe Business do
it_behaves_like 'an UnnamedSimpleSerializable'
# ...
end
# etc...
let(:invoice) { Invoice.new }
let(:shopper) { build :shopper }
let(:account) { create :account }
prefer blank objects
prefer non-persisted objects
rethink your tests otherwise
spec/adapters/api/v1/job_loader/job_loader_spec.rb
spec/adapters/api/v1/pitchset_loader_spec.rb
spec/requests/api/v1/pitchsets_spec.rb