Using semantic meaning in features and user interfaces
WORK IN PROGRESS
This explains why I need to fork webrat, and why I believe features should rely upon css classes and id's.
I believe that the CSS class is generally the best mechanism for expressing the semantic meaning of a particular piece of html (combined with DOM location and html tags). For example if somehow in viewing an order we want to reorder the order then I would first produce
.order
.reorder
# re-order goes here
This clearly establishes the semantic meaning - that I can reorder an order. Now the actual UI could be a number of things
- Instructions:
Reorder by phoning 0800 555 5555 and quoting order# - a button
[reorder] - a link
click <here> to reorder
Now as far as my business requirement and feature is concerned I don't care about these implementation details, and these details are certainly volatile. However the semantic layout is fixed. Here I see a representation of an order and can reorder the order.
I can now refine this feature to state that I should be able to tell the order to reorder. When I do this I should see some response in the UI which indicates whether my command has been successful.
When I reorder
Then I should see ...
In web applications you can do this in two ways
- follow a link
- submit a form (by pressing a button)
Again my feature does not care how I reorder unfortunately with webrat I now have to choose - I can either 'click' or 'press'.
.order
.reorder
%a{:href => order_path(:id => order.id)}= h(order.name)
above I am 'clicking' a link, whilst below I am pressing a button
.order
.reorder
-form_for @order do |f|
...
f.submit "reorder"
Now as far as my feature is concerned I don't give a damn how the re-ordering is done I just want to reorder. However I have to change my scenario (yes I know I can hide this change to a degree by re-using steps and having fancy step definition).
Scenario: Reorder
Given there is an order
When I view orders
And I click reorder
Then ...
OR
Scenario: Reorder
Given there is an order
When I view orders
And I press reorder
Then ...
Webrat compounds this frustration by making it really hard to press or click. Currently it will click only using text, id or an href, and its even more restrictive when it comes to pressing. So my first improvement was to extend click link to also be able to use classes. Now at least I can do
click link reorder
Although I have to change my implementation code
.order
%a{:clases => reorder, :href => order_path(:id => order.id)}= h(order.name)
Features for a Resource
REST is great, and resources are a great design tool. If we design our rails application around resources we can get alot of things for free. If we apply the same idea to features can we create a standard set of features to give us a quick start for each resource we identify?
Resources - Object and Collection
With standard rails routing we will get various named routes which we can use either directly in our features or indirectly in our step definition
For example if our routing table has
new_admin_product GET /admin/products/new
destroy_admin_product GET /admin/products/:id/destroy
edit_admin_product GET /admin/products/:id/edit
admin_product GET /admin/products/:id
products GET /products
product GET /products/:id
Then we can easily write
When I view products
and match this with
When /^I view product$/ do
visit products_url
end
In a similar way we can do things like
Given there is a product with name widgit
When I view the product
matched with
Given /there is a product with name (\S+)$/ do |name|
Product.generate!(:name => name)
@x = Product.find_by_name(name)
@x.should_not be_nil
@x
end
When /^I view the product$/ do
# use the variable x from above
visit product_url(@x)
end
Its fairly straightforward to extend these features and steps to match the other actions, and with a bit of ruby and regex fu we should be able to generalise these features so we can use them with any resource
Resource Views
The simplest way to do this is to use CSS classes. I take this approach because it is robust. Object are represented in a div with the singular name as a class and collections use the plural. e.g. in Haml
.product
product details go here
.products
list of products go here
Resources - Info (A Basic Data Definition)
I think it might be a nice idea to describe a resource on its list page using some text in an div#info. e.g.
#info
Products are things that we actually sell. They come from wholesale_products which are supplied to us.
.products
This is just a little convenience which will be replaced/hidden when we start doing specific work with the resource.
Composing Features
Composing Features
Writing good features is an art-form, a very new art-form that people are just beggining to explore. Of course people have been specifying how things should work throughout history, and people have been trying to specify what software should do (with varying degrees of failure!) since software has been written.
You can compare writing features to a new school of art e.g. pontillism. Pontillism built itself on the existing foundations of painting and art - in particular colour theory - and applied these using a new technique to create something a little different. In our new school of features we produce something a little different (a plain text executable) with a new tool (Cucumber) and the application of old and new techniques. To do this well we have to learn what works and what doesn't.
Asking Questions
When we write a feature we need to ask lots of questions. A feature is an exploration of something we want to do. Starting a feature is focused on two things, loosely describing the feature and asking the most important question.
- Why do we want to do this?
The first temptation when writing features is to skip this question! After all we know we need the feature, otherwise we wouldn't be writing it in the first place. And we want to get on with things! What's the point in wasting time on this question?
Well your about to commit a substantial amount of your time writing this feature and a substantial amount of other peoples time in implementing this feature. If it takes you 20 minutes to answer this question properly and you discard one in fifty of your potential features you will still have saved loads of time and effort.
To help you answer this question you can consider the following questions
- Whats the point?
- Can it wait?
- Is there something more important we need to do first?
Writing Scenarios
Once we get to the stage of writing scenarios, we should be fairly confident that we are not going to be wasting our time writing the scenarios. In addition to specifying what should be done, writing scenarios help us to discover things about our feature. As we write our feature we should be open to the following ideas
- We need to do something else first!
- We don't need to do this now!
- This is pointless!
- We don't need this!
Hopefully the point is made now!
Writing Scenarios (the details)
As you write a scenario its a good idea to ask a few more questions.
- What am I assuming?
- Where does this come from?
- Is this a compound statement?
- Do I know what this really means? And even if I do can anyone else?
- Who can do this?
- Who should not be allowed to do this?
- Is this name ambiguous, clumsy, accurate, intuitive?
To write scenarios efficiently we need to develop the skills to ask the write questions at the right time. A warning though - not asking questions, guarantees you will write rubbish, be innefficient, waste other peoples and never develop the skills to write scenarios quickly. Remember this is an art form you have to really work at it to get good.
I'll try below to give some illustrations of applying these questions
Examples (ECommerce Products)
NOTE: Just sketching out things here for the moment
Scenario: A product should
Illustrate composition using VAT Ask the questions
Where does the Supply Price come from? Is it more important to write about this first? Where does the VAT rate come from.
Stop writing this feature and write the feature that sets the VAT rate. As your doing this ask
How many VAT rates? What happens when the VAT rate changes? - to new sales, to old sales How do we change the VAT rate
- write the feature
Feature As a site admin I need to change the VAT rate for all products In order to comply with the national changes to VAT caused by the credit crunch
Do we need to keep the VAT rate with each purchase?
Is the VAT rate
If the supply price comes from
Complications with users and customers
I find it very easy to get paralysed by complexity. This has been happening on my current project with users and customers. You see I want people to be able to buy things without having to be users. However I also want it to be especially easy for users to buy things. Finally I don't want any particular person to have seperate records in user and customer but a customer can be user.
Now this doesn't really make sense, and I haven't really written the half of it! And all these possibilities have ... well to be frank left me paralysed. Now eventually I might get my head around all of this but is there a better way? Can I use features to explore this problem bit by bit, just dealing with one little piece at a time. Will a reasonable solution emerge
Here is the first scenario I'm going to use
Scenario: Customer with existing user
Given there is an activated user shirley
Given I am logged out
And I am asked for my customer details
When I fill in my customer details with shirleys details
And I press "next"
Then I should see an error explanation
This scenario is at a pretty high level and implemented using a number of compound steps.
What this does is go through a number of things to get to a form where we enter in the details required to become a customer. This is part of a wizard, after the customer step comes the billing step.
So what I'm saying is that if I fill in my customer details with an existing user then I should get an error with an explanation. Making this story pass was actually very simple, perhaps I have made a bit of progress with this problem - so what next?
Well lets assume the error explanation tells the user the correct thing to do then we can
When I login as shirley
Then I should be at billing step
When I login as shirley has a number of design implications which we can now deal with. These are made real by implementing the step. With the stuff I have so far the step would be implemented by
When /^I login as shirley$/ do
visits login_path
fills_in(:login, :with => @shirley.login)
fills_in(:password, :with => @shirley.password)
clicks_button
end
And to make my story work I have to go back to where I was
When I login as shirley
And I go to checkout
Then I should be at billing step
Getting this to work is a good thing. But now I really need to refine this as my customer has to
- visit the login path
- fill in the login form and submit it
- return back to the checkout
and this is far to much to expect. So going back to our original story
When I login as shirley
Then I should be at billing step
we can see that we want to implement When I login as shirley as
When /^I login as shirley$/ do
fills_in(:login, :with => @shirley.email)
fills_in(:password, :with => @shirley.password)
clicks_button
end
which means we need a login form on our page, and this form needs the ability to login using an email. A couple of options come to mind here
- Implement the login form as part of the error explanation with the login field filled in with the email address.
- Have the login form on every page and point the user to it with the error explanation.
So I'll have a think about this for a while; but note how we have made some progress, have a simpler decision in front of us, and have some code which is testing what we are doing and will show whether our solution is working.
Update
By setting
after_filter :store_location, :only => [:edit]
in the OrdersController I can now make
When I login as shirley
Then I should be at billing step
pass, as when I return from the login form to the wizard when I log in. So for now I can postpone the decision about having a login form on this page or on every page and have the alternative of putting some better instructions in response to the email in use error.
To deal with login with email I created a step
When /^I login as (\S+) with my email address$/ do |login|
used this step in the scenario
and implemented the functionality by changing how User authenticates from
def self.authenticate(login, password)
u = find_in_state :first, :active, :conditions => {:login => login} # need to get the salt
u && u.authenticated?(password) ? u : nil
end
to
def self.authenticate(login, password)
if RE_EMAIL_OK.match(login)
u = find_in_state :first, :active, :conditions => {:email => login} # need to get the salt
else
u = find_in_state :first, :active, :conditions => {:login => login} # need to get the salt
end
u && u.authenticated?(password) ? u : nil
end
n.b. RE_EMAIL_OK is a regex
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
Features Controllers and HTTP_REFERER
Had a problem with a feature failing. Was getting an error
When I add a product to the cart # features/steps/cart_steps.rb:11
Cannot redirect to nil! (ActionController::ActionControllerError)
/Library/Ruby/Gems/1.8/gems/actionpack-2.1.1/lib/action_controller/base.rb:1044:in `redirect_to'
The additional huge stack trace led me to my cart controller. Here I was using the rather standard sort of code
respond_to do |format|
format.html { redirect_to request.env["HTTP_REFERER"]}
end
Problem is that in the test environment request.env["HTTP_REFERER"] is nil. We have a couple of choices
- Set HTTP_REFERER in the test environment
- Program defensively to deal with nil HTTP_REFERER
In the end did the second (mostly because couldn't get first to work quickly)
respond_to do |format|
format.html { redirect_to referer}
end
...
private
def referer
ref = request.env["HTTP_REFERER"]
ref ||= '/products'
end
I actually quite like this solution.
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'