Sunday, May 12, 2013

RSpec: Simple Tests to Learn its Behavior

Few people would argue that making it a point to ensure some level of testing is in place to verify that the functionality built works as expected is a bad practice. Size and complexity will typically dictate the testing philosophy used on a project. I tend to balance the effort required to build and maintain the automated tests versus the effort required to manually retest a module when a change is introduced. Fortunately, RSpec has a lot of power available for enabling reuse, readability, and, most importantly, brevity. All good things for developers trying to meet tight deadlines. Generally, I attempt to keep it as simple as possible by carefully orchestrating the basic let()/it() combined with good use of context to build most of my tests. Granted, when I wrote my first test, I was definitely fighting the expected philosophy of RSpec. Once I started to understand the behavior of each of its components, the pieces started falling into place and devising examples became a lot easier. Here's a few of trials I used to gain more clarity around how the different components work together.




Using context to control scope



Context creates scope for the examples such that let() definition in other contexts can't interact with the current context:


describe "let() scope" do

# Visible to both context blocks
let(:var1) { 3 }

context "block 1" do
# Only visible to this block
let(:var2) { 5 }

# Error
specify { var3.should eq(7) }
# Ok
specify { var2.should eq(5) }
specify { var1.should eq(3) }
end

context "block 2" do
# Only visible to this block
let(:var3) { 7 }

# Error
specify { var2.should eq(5) }
# Ok
specify { var3.should eq(7) }
specify { var1.should eq(3) }
end

end



Running this test results in these failures:



1) let() scope block 1
Failure/Error: specify { var3.should eq(7) }
NameError:
undefined local variable or method `var3' for #<RSpec::Core::ExampleGroup::Nested_1::Nested_1:0xb6b78ef8>
# ./spec/test_spec.rb:14

2) let() scope block 2
Failure/Error: specify { var2.should eq(5) }
NameError:
undefined local variable or method `var2' for #<RSpec::Core::ExampleGroup::Nested_1::Nested_2:0xb6b763d8>
# ./spec/test_spec.rb:25




Redefine let() value



Define the value once and redefine it as necessary. Context blocks can work with the default or override it to something different for the given context without affecting another context:


describe "let() redefined" do

let(:var1) { 3 }

context "block 1" do
let(:var1) { 5 }

specify { var1.should eq(5) }
end

context "block 2" do
let(:var1) { 7 }

specify { var1.should eq(7) }
end

# Unaffected by block 1 and 2
context "block 3" do
specify { var1.should eq(3) }
end
end



Cascading usage of let() values



Use previous let() values to define other values in a given context. This enables reuse and readability throughout the different contexts and examples:


describe "let() reuse" do

let(:var1) { 3 }

context "block 1" do
let(:var2) { var1 + 5 }

specify { var2.should eq(8) }
end

context "block 2" do
let(:var2) { var1 + 7 }

specify { var2.should eq(10) }
end

end



Be aware of recursion



Values created by let() are really just methods which will be called when referenced. Trying to define the same value again by referencing itself will cause a recursion error:


describe "let() recusive" do

let(:var1) { 3 }

context "block 1" do
# Fail
let(:var1) { var1 + 5 }

specify { var1.should eq(8) }
end

end


You'll see something like this. In more complex tests, these might be hard to find. Until it completely clicked what let() was really doing, I didn't entirely realize what I had done:


3) let() recusive block 1
Failure/Error: let(:var1) { var1 + 5 }
SystemStackError:
stack level too deep




Obviously, these are quite simple. However, I keep a little scratch pad of simple examples that I can tinker with and try out ideas to see the behavior before trying it out in a larger, more complex test script. Once you start adding in a database environment, application state, etc, it becomes difficult to stay true to the simple principles available in the test suite. Sometimes, going back to basics allows you to see things more clearly and apply them in other contexts.