RSpec, meet Flow.
Flow is the demon that appears after about an hour of using RSpec. He sits on my shoulder and watches me code.
After a few minutes he stands up. I know to turn down the music. He whispers into my ear. I nod in agreement and mouth back a response.
Flow understands my frustration with testing individual methods outside any greater context or usage scenario.
RSpec is a beautiful tool. Leaps and bounds ahead of the standard testing framework built into Rails. It does this, I believe, by allowing you to use more natural language. But it can be be misused. Which is actually closer to abuse, because unless you’re paying respect to flow, you’re fucking yourself.
Don’t test methods outside of the context in which they should be used. It doesn’t mean shit to the person reading your code — even if that person is you.
Your program is a play. Your objects are actors. You are the writer and director. You say action! You say cut! You know all the lines, but it’s not your job to voice them. You assign those roles to your actors, and they carry it out much better than your sorry ass ever could.
Context. Acknowledge it. If your tests look like this:
describe 'An InterestList' do before(:each) do @interest_list = InterestList.create end describe 'upon creation' do it 'should not be a new record' it 'should be empty' it 'should report having zero Interest objects' end describe 'when adding a ProductInventoryPrototype' do before(:each) do @cheese = inventory_prototypes(:cheese) end it 'should answer true' end describe 'with one ProductInventoryPrototype in it' do before(:each) do @cheese = inventory_prototypes(:cheese) @interest_list.add(@cheese) end it 'should not be empty' it 'should report having one interest' end describe 'removing a ProductInventoryPrototype' do before(:each) do @cheese = inventory_prototypes(:cheese) @interest_list.add(@cheese) end it 'should answer with the item when that item is removed' end describe 'after removing a ProductInventoryPrototype' do before(:each) do @cheese = inventory_prototypes(:cheese) @interest_list.add(@cheese) @interest_list.remove(@cheese) end it 'should be empty when that item is removed' it 'should report having zero items' end end
You’re fucking with the laws of nature, and you will be smitten.
What’s wrong with it? The
#before
code is reinitializing all the steps you’ve just accomplished.
Make it look like this.
describe 'When working with an InterestList,' do before(:each) do @interest_list = InterestList.create end describe 'it must first be created.' do it 'It should not be a new record.' it 'It should be empty.' it 'It should report having zero Interest objects.' describe ' After which you can add a product.' do before(:each) do @cheese = inventory_prototypes(:cheese) end it 'To which it should answer true.' describe 'It now has one product in it.' do before(:each) do @interest_list.add(@cheese) end it 'It should not be empty.' it 'It should report having one interest.' describe 'Now we remove a ProductInventoryPrototype.' do it 'It should answer with the item when that item is removed.' describe 'After removing a ProductInventoryPrototype,' do before(:each) do @interest_list.remove(@cheese) end it 'it should be empty when that item is removed.' it 'it should report having zero items' end end end end end end
Of primary importance is a subtle point. I didn’t illustrate it in the two examples above, but I will now:
Before:
describe 'When working with an InterestList,' do before(:each) do @interest_list = InterestList.create end describe 'it must first be created.' do it 'It should not be a new record.' it 'It should be empty.' it 'It should report having zero Interest objects.' describe ' After which you can add a product.' do before(:each) do @cheese = inventory_prototypes(:cheese) end it 'To which it should answer true.' it 'It should not be empty.' it 'It should report having one interest.' ...
After:
describe 'When working with an InterestList,' do before(:each) do @interest_list = InterestList.create end describe 'it must first be created.' do it 'It should not be a new record.' it 'It should be empty.' it 'It should report having zero Interest objects.' describe ' After which you can add a product.' do before(:each) do @cheese = inventory_prototypes(:cheese) end it 'To which it should answer true.' describe ' At which point' do it 'it should not be empty.' it 'it should report having one interest.' ...
On the surface it’s just another level of nesting. After meditation, you realize you are first testing the object’s answer to your message. Second, you are testing the state of the universe after that action is made. The difference is subtle, but important. If you don’t understand the importance, recall the indifference you felt when you first saw RSpec: it’s Rails’ standard test framework with different words thrown in.
You were wrong then and you’re wrong now.
Words offer the means to meaning, and for those who will listen, the enunciation of truth. - V
If an object’s answer to the message passed violates expectations, flow stops. You know, not that a method is broken, but that flow is broken.
Of secondary importance is that your
#before
methods are no longer redundant.
The result of running your tests now looks like:
When working with an InterestList, it must first be created. It should not be a new record. (Not Yet Implemented) When working with an InterestList, it must first be created. It should be empty. (Not Yet Implemented) When working with an InterestList, it must first be created. It should report having zero Interest objects. (Not Yet Implemented) When working with an InterestList, it must first be created. After which you can add a product. To which it should answer true. (Not Yet Implemented) When working with an InterestList, it must first be created. After which you can add a product. It now has one product in it. It should not be empty. (Not Yet Implemented) When working with an InterestList, it must first be created. After which you can add a product. It now has one product in it. It should report having one interest. (Not Yet Implemented) When working with an InterestList, it must first be created. After which you can add a product. It now has one product in it. Now we remove a ProductInventoryPrototype. It should answer with the item when that item is removed. (Not Yet Implemented) When working with an InterestList, it must first be created. After which you can add a product. It now has one product in it. Now we remove a ProductInventoryPrototype. After removing a ProductInventoryPrototype, it should be empty when that item is removed. (Not Yet Implemented) When working with an InterestList, it must first be created. After which you can add a product. It now has one product in it. Now we remove a ProductInventoryPrototype. After removing a ProductInventoryPrototype, it should report having zero items (Not Yet Implemented)
This is easier to read and understand than a series of isolated tests. You’re made aware of the context in which each test is being run, which is priceless when you’re attempting to understand the system from a high level.
Congratulations. You are no longer a moron coding outside the realm of the context in which your code lives.
