Skip to content

Instantly share code, notes, and snippets.

@davetron5000
Created October 1, 2011 01:11
Show Gist options
  • Save davetron5000/1255458 to your computer and use it in GitHub Desktop.
Save davetron5000/1255458 to your computer and use it in GitHub Desktop.
WTF RSpec?
# Here's the example from rpsec-given
#
# Initially, I thought I'd like the Given/When/Then
# replacement for RSPec's cruddy "it", but I don't
# I find this test very hard to follow, as there are
# several statements that create state via variables
# in a subtle and unclear way. Further, we have some
# strange indirection with the "stack_with(initial_contents)".
# It seems like we're adding a lot of complexity just to save
# a simple variable assignment.
#
# Read through and meet me at the bottom.
require 'rspec/given'
require 'spec_helper'
require 'stack'
describe Stack do
def stack_with(initial_contents)
stack = Stack.new
initial_contents.each do |item| stack.push(item) end
stack
end
Given(:stack) { stack_with(initial_contents) }
context "when empty" do
Given(:initial_contents) { [] }
Then { stack.depth.should == 0 }
context "when pushing" do
When { stack.push(:an_item) }
Then { stack.depth.should == 1 }
Then { stack.top.should == :an_item }
end
end
context "with one item" do
Given(:initial_contents) { [:an_item] }
context "when popping" do
When(:pop_result) { stack.pop }
Then { pop_result.should == :an_item }
Then { stack.should be_empty }
end
end
context "with several items" do
Given(:initial_contents) { [:second_item, :top_item] }
Given!(:original_depth) { stack.depth }
context "when pushing" do
When { stack.push(:new_item) }
Then { stack.top.should == :new_item }
Then { stack.depth.should == original_depth + 1 }
end
context "when popping" do
When(:pop_result) { stack.pop }
Then { pop_result.should == :top_item }
Then { stack.top.should == :second_item }
Then { stack.depth.should == original_depth - 1 }
end
end
end
# Oy. I do not like this. I find it difficult
# to figure out what's being tested. Particularly confusing
# is the "original_depth" variable. It seems that
# the bang-version of Given, Given!, has the side
# effect of assigning the variable each time it's
# accessed. This, to me, is a clear indication that
# the abstraction is breaking down. Further, this entire
# flow wreaks havoc with my ability to figure out
# what's going on and what gets assigned when. I'm not
# even convinced this isn't a bug in the example.
#
# I have to ask myself: what are we gaining by
# changing the semantics of "a bunch of lines of code"
# In almost all Ruby code, things execute from top to bottom
# and you can easily figure out where variables
# come from and how they get their values. Here,
# we have the value of what appears to be a variable (but
# is actually an accessor method [scoped to where?!]) getting
# its value from a potentially very long way away, and where
# it's value is very dependent on the state of the test.
# Why?
#
# Let's rewrite this, keeping the "should" stuff for now, using
# Test::Unit.
require 'stack'
# Here, we test in plain Ruby using Test::Unit style.
# There's no magic, no metaprogramming, no alternative
# means of assigning variables or decomposing things
# We retain the RSPec matchers to facilitate comparison to the original
# Read through, comments below
class StackTest
test "when empty, stack should have no depth" do
empty_stack.depth.should == 0
end
test "pushing to stack" do
stack = empty_stack
stack.push(:some_item)
stack.depth.should == 1
stack.top.should == :some_item
end
test "popping the stack" do
stack = stack_with([:some_item])
pop_result = stack.pop
pop_result.should == :some_item
stack.should be_empty
end
test "stack with many items" do
stack = stack_with([:second_item, :top_item])
original_depth = stack.depth
stack.push(:new_item)
stack.top.should == :new_item
stack.depth.should == (original_depth + 1)
pop_result = stack.pop
pop_result.should == :new_item
stack.top.should == :second_item
stack.depth.should == original_depth
end
private
def stack_with(initial_contents)
stack = Stack.new
initial_contents.each do |item| stack.push(item) end
stack
end
def empty_stack; stack_with([]); end
end
# This is much easy to follow, with our testing goals
# simply written in plain English rather than broken
# RSpec-ese. Further, we know how all variables get their
# values, and we can decompose things using plain Ruby.
#
# There is still an issue, however. The "should" style
# of assertions leave valuable information on the floor.
# Specifically, when we assert things about derived values
# of our stack, namely the depth, we lose all diagnostic
# information that might explain what's going on. Namely
# The stack; since it's depth isn't what we expect, examining
# the stack will certainly help figure out what went wrong.
#
# Let's get rid of the rspec matchers entirely and, using
# regular Ruby methods again, write our tests using assertions
# with failure explanations
require 'stack'
# Here, we replace all the RSpec matchers with
# Test::Unit assertions, including helper assertions that we
# make to keep what's being tested clearer
#
# Read through, summary below
class StackTest
test "when empty, stack should have no depth" do
assert_empty empty_stack
end
test "pushing to stack" do
stack = empty_stack
stack.push(:some_item)
assert_depth stack, 1
assert_top stack,:some_item
end
test "popping the stack" do
stack = stack_with([:some_item])
pop_result = stack.pop
assert_equal :some_item,pop_result
assert_empty stack
end
test "stack with many items" do
stack = stack_with([:second_item, :top_item])
original_depth = stack.depth
stack.push(:new_item)
assert_top stack,:new_item
assert_depth stack, original_depth + 1
pop_result = stack.pop
assert_equal :new_item,pop_result
assert_top stack,:second_item
assert_depth stack, original_depth
end
private
def stack_with(initial_contents)
stack = Stack.new
initial_contents.each do |item| stack.push(item) end
stack
end
def empty_stack; stack_with([]); end
def assert_depth(stack,depth)
assert_equal depth,stack.depth,"Stack was #{stack.to_s}"
end
def assert_empty(stack)
assert_stack stack, :depth => 0
end
def assert_top(stack,expected_item)
assert_equal expected_item,stack.top, "Stack was #{stack}"
end
end
# While the size of the code has increased, our tests
# couldn't be clearer, and the way they work couldn't
# be simpler. We also have some extractable assertions that
# we could use in other tests if we need to; we can simply
# extract them into a module.
#
# All that being said, the Given/When/Then stuff adds
# some decent structure to each test; it makes it
# clear what's setup, what's execution, and what's
# the assertions. Let's add that in and see how we like it
require 'stack'
# Here, we add back the given/when/then markers.
# This will delineate the sections of our tests.
# We're still in very basic Ruby, with no
# shenanigans.
#
# Read through, summary below
class StackTest
include GivenWhenThen # see below
test "when empty, stack should have no depth" do
assert_empty empty_stack
end
test "pushing to stack" do
Given
stack = empty_stack
When
stack.push(:some_item)
Then
assert_depth stack, 1
assert_top stack,:some_item
end
test "popping the stack" do
Given
stack = stack_with([:some_item])
When
pop_result = stack.pop
Then
assert_equal :some_item,pop_result
assert_empty stack
end
test "stack with many items" do
Given
stack = stack_with([:second_item, :top_item])
original_depth = stack.depth
When
stack.push(:new_item)
Then
assert_top stack,:new_item
assert_depth stack, original_depth + 1
When
pop_result = stack.pop
Then
assert_equal :new_item,pop_result
assert_top stack,:second_item
assert_depth stack, original_depth
end
private
def stack_with(initial_contents)
stack = Stack.new
initial_contents.each do |item| stack.push(item) end
stack
end
def empty_stack; stack_with([]); end
def assert_depth(stack,depth)
assert_equal depth,stack.depth,"Stack was #{stack.to_s}"
end
def assert_empty(stack)
assert_stack stack, :depth => 0
end
def assert_top(stack,expected_item)
assert_equal expected_item,stack.top, "Stack was #{stack}"
end
end
module GivenWhenThen
def Given; end
def When; end
def Then; end
end
# OK, whoa. The Given/When/Then is just
# a bunch of no-ops?
# SURE! Why not? We don't need it to do anything
# and there's nothing gained by giving them a block, except
# that we complicate our ability to communicate between
# the different sections (this is why RSpec has to have
# that bizzarre "let" stuff)
#
# Note that we don't use this for our one-liner; there's
# no real value add there.
#
# So, we could've just done this with comments, right?
# Well, with this in place, we *could* do some more fancy
# things, like so:
module StrictGivenWhenThen
@state = nil
def Given
raise "Did you finish your last test?" unless @state.nil?
@state = :given
end
def When
raise "No Given?" unless @state == :given
@state = :when
end
def Then
raise "No When?" unless @state == :when
@state = nil
end
end
# OK, so we can enforce using these sections
# in our tests. We might want to do this, who knows?
#
# Ultimately, the point is that we can get the benefits RSpec
# and rspec-given claim to provide by just using
# simple Ruby; Ruby that anyone can understand, and that
# results in code that we can organize the best way
# possible, rather than being constrained to RSpec's
# crazy world of pseudo-assignments, include-the-world-global-namespacing
# and other baggage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment