-
-
Save boutros/3065245 to your computer and use it in GitHub Desktop.
WTF RSpec?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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