Features, Specs and Maps
The Map is Not The Territory
I first came across General Semantics in the works of A.E. Van Vogt. His stories from sci-fi's 'golden age' had a common theme about the infinite potential of 'the human nervous system' which was very appealing to a brainy weedy kid. Part of the development of human kind, in these stories, involved a rejection of Aristotelian thinking. This Non-Aristotelian thinking is based upon Korzybski's General Semantics. But wait a minute what has this to do with code and RSpec and Cucumber?
One of General Semantics keys is "the map is not the territory", and that is what this article is all about. Looking at features and specs as maps helps me understand how to use these tools more effectively and also addresses some common frustrations that people new to BDD and these tools experience. But first a little aside into frustration and why its such a good thing.
Frustration is good - read the signs!
Frustration is good! What are you talking about!!
Frustration is your brains way of telling you that something is fundamentally wrong. Your brain is saying STOP! Think Again! So what do we do? ... Well most of the time we ignore our brain, or we blame something/someone, or we 'work our way through it'.
See frustration as a warning light on the dashboard of a car. Don't ignore it or you'll "run out of fuel", "blow up your engine" etc. Frustration is a great early warning system, telling you that you don't really now enough about what you are doing. So stop and use the most formidable thinking machine on the entire planet - your brain!
Features as Maps
If a feature is a map what is the territory? Well I think features are a two way map between territories. These territories are
- The intentions of the application sponsor (the person or persons deciding that the application should be built and what its contents will be)
- The application
The Qualities of Maps
All maps have a scale, which performs two functions
- Show the distance represented by any two points on the map
- Determine the level of detail shown on the map
Features have a large scale, they cover a wide area with little detail. They have to be this way. The first territory they map is a vague amorphous entity which cannot be seen, touched or expressed particularly easily. This is difficult terrain to map so features use (as much as they can) one of our most powerful tools - natural language. The second terrain they map is a small precise highly defined territory. In fact it is so precise that it can be accurately described and even executed using just two characters 0 and 1. Features don't really make much of an attempt to map this terrain. They are more interested in executing (in the computational sense) the terrain. However they can be quite useful as a map of other maps of the application, its UI and source code.
Feature Frustrations
This article was inspired by frustration expressed in a post on the Cukes mailing list. This frustration (IMO) is based on not understanding the scale of features map. Detail is not appropriate in features because detail is not in the territory of the application sponsor. If developers pollute the feature map with their level of detail then the application sponsor can no longer read the map. The map becomes too large and to dense.
Fortunately features have other important functions apart from being a map, so it is reasonable (essential?) to compromise on what they show. But once we understand how detrimental detail is to features then it becomes obvious that they must be supplemented with other tools.
Writing this has made obvious something that I was barely aware of that is that software has to be specced and tested by a suite of tools. An application is too complex a territory to have just one map. Once it becomes obvious that an application cannot be mapped by one map it is a small leap to realise that an application cannot be tested with one test tool, specced with one language. Just as we have to map at different levels we have to execute and test at different levels. I used to be frustrated by having to combine features with specs, and then think about controller specs, helper specs, view specs, user acceptance testing ... Now instead of being frustrated I realise that the need to learn a suite of tools and the necessity of creating a range of maps is just a natural result of the character of the territory I am working in.
Searching for empty form fields using rspec
Took me far to long to work out how to do this!
To find an empty input field we can look for
"input[type=text][value='']"
e.g.
response.body.should have_tag("input[type=text][value='']")
This is useful when filling in forms badly to make sure your bad values still remain when you return to the form to edit them.
Pluralisations in features
Been a bit of a dummy in this one. Basically each step definition is a regular expression so if we have
When /^I add (\d+) products to the cart$/ do |n|
we can improve this to make allow both
When I add 1 product to the cart
When I add 2 products to the cart
Problem testing for empty div
Assert_Select is the main Rails tool for testing HTML output. RSpec wraps this function with its have_tag and with_tag methods. Using these methods effectively is key to writing features. However Assert_Select has some problems which caught me out recently.
What I wanted to do is make sure my cart could not be seen when empty
Scenario: Cart is hidden if empty
Given there are 4 products
And I am on the products page
And the cart is empty
Then I should not see the cart
The difficulty was with Then I should not see the cart. The output I get from an empty cart is
<div id='cart'>
</div>
The empty div is there so I can populate it using AJAX at some point. In rspec terms the following statements should be equivalent
response.should_not have_tag("div#cart>*")
response.should_not have_tag("div#cart") do
with_tag("*")
end
response.should have_tag("div#cart") do
without_tag("*")
end
However only the first one works. Translating into Assert_Select we get
assert_select("div#cart") do
assert_select("*",0)
end
assert_select("div#cart", 0) do
assert_select("*")
end
Neither of these work. See rspec bug
Update
This was invalid when given a block assert_select tries to match the element specified (in this case div#cart) and then yields the matches. so you should be doing
assert_select("div#cart") do |elements|
elements.each {|element| assert_select("*",0)}
end
My previous code on the other hand asserts that div#cart exists and then asserts that there are no elements at the top level rather than asserting that there are no elements that are children of the match.
Thanks to Frederick Cheung for this see rails bug
Paths in features
Been thinking about specifying paths in stories and steps, and the brittleness they can introduce to tests. This was prompted by a bug in rspecs 'render_template' matcher.
First of all the bug ...
This is rails testcode that works ok
assert_template("/")
This is the rspec equivalent
response.should render_template("/")
which throws the exception
wrong argument type nil (expected Regexp) (TypeError) /Library/Ruby/Gems/1.8/gems/rspec-rails-1.1.8/lib/spec/rails/matchers/render_template.rb:22:in `match'
/Library/Ruby/Gems/1.8/gems/rspec-rails-1.1.8/lib/spec/rails/matchers/render_template.rb:22:in `matches?'
Paths Don't Belong In Stories
Feeling more convinced about this the more I think about it. Basically a specific path is an implementation detail which can change depending (obviously) on how you implement. So if you want to view some products your story might be something like
When I click products
Then I should see products
This is more robust than
When I click products
Then I should go to /products
In a similar way when writing the steps you should use the products_path method rather than using '/products'
Getting started with Cucumber
Cucumber is the new runner for Rspec plain text stories. It has a number of improvements that make it essential, and it will soon become the default runner for rspec. To get started we need to do a few things
Install the gem
sudo gem install cucumberRun the generator for each rails project you want to have cucumber in.
script/generate cucumberUpdate my command in
~/.profilethat runs rails projects in textmate to include thefeaturesfolder, andsource ~/.profile## this is ~/.profile ... # edit this directory for rails projects (etdr) alias etdr='mate app config features lib db public spec stories test vendor/plugins &'
Most importantly cucumber
- tells which matcher failed when a story fails
- throws an error if there are ambiguous matchers
Setting Up Textmate
Couple of bundles to install
cd ~/Library/Application\ Support/TextMate/Bundles
git clone git://github.com/bmabey/webrat-tmbundle.git Webrat.tmbundle
git clone git://github.com/bmabey/cucumber-tmbundle.git Cucumber.tmbundle
Understanding Plain Text Story File Hierarchy
Plain text stories have quite alot of infrastructure that allows you to run the story file. I'm being pretty dumb in understanding this, so I'm going to analyse whats going on in detail so I have a better idea of what is being done and why.
I learnt some important lessons from doing this.
If you don't understand a line of code break it down until you do. This will improve your ruby.
irbis your friend, use it andscript/consoleto explore code you don't understandEven though it takes time, exploring code is far more productive than skipping over it.
To start with I'll have to analyse various ruby statements in some detail. The first is from visitor.rb
require File.join(File.dirname(__FILE__).gsub(/stories(.*)/,"stories"),"helper")
require:loads a library, takes a path and tries to load whatever is there.File: abstracts a file object - can be file, dir, symlink.File.join:takes a bunch of strings and joins them together using the default file seperator (File::SEPARATOR)
From the above we can see that it looks like we are going to join a couple of strings together and that the result will end in helper
__FILE__:name of file containing current code being executed. This will bevisitor.rbFile.dirname(filename)gives the directory containing the file.File.dirname(__FILE__)this gives the directory of the file containing the code being executed. This directory will be dependent on where the code is being executed from. So if we call the file from the directory its in we will get".". However if we call from the directory above, and the directory the file is in is called 'story' then we will get"story". The return value will be a String
-- gsub(pattern, replacement) :this is a method of String, that returns a copy of the string with all occurences of pattern replaced by replacement
-- gsub(/stories(.*)/,"stories" :this will remove anything that follows stories in the string.
We know can work out a big part of this code
File.dirname(__FILE__).gsub(/stories(.*)/,"stories")
The idea here is that the file this code is in is somewhere in a file hierarchy under a directory called stories. What we want to do is go up to that directory, and we want to do that in a way that is independent of how far down we may be in the hierarchy. The following illustrates this (run in irb)
>> 'stories/stories/ppo'.gsub(/stories(.*)/,"stories")
=> "stories"
Now we can grok the whole line. The join will concatenate '/helper' giving stories/helper. So the line of code will load the code in the file helper which is located in the stories folder somewhere above this file.
Textmate File Type Detection (RSpec & Rails)
See this Textmate Blog post
Texmate RSpec Bundle
Do following
cd ~/Library/Application\ Support/TextMate/Bundles/
git clone git://github.com/dchelimsky/rspec-tmbundle.git RSpec.tmbundle
Then reload bundles in textmate if its open.
Adding RSpec to Rails Project using Git
Quick reminder how to do this...
Pre-Requisites
- rails project
- using git
git init .
Installation
Plugins go in vendor/plugins
git submodule add git://github.com/dchelimsky/rspec.git vendor/plugins/rspec
git submodule add git://github.com/dchelimsky/rspec-rails.git vendor/plugins/rspec-rails
Then run
script/generate rspec
Note: You need to run the following two commands (or at least the last one) every time you clone this repo.
git submodule init
git submodule update
Older posts: 1 2