Cucumber and the global scope problem - bring on page objects and the MGF pattern
There is no argument, Cucumber has added a new dimension to acceptance testing, Gherkin Features are living executable requirements and that for me is the benefit we should never loose sight of.
So you’ve clocked this and created a bunch of automated tests driven from you’re features….but can too much Gherkin be a problem?
In my experience it can and it’s something you need to keep a close eye on to avoid you’re tests becoming as big a maintenance burden as the application code.
The Global Scope Problem
For a project with just a few features you can quite happily get away with a project structure that looks something like that below. All your steps in a single file containing the automation code for your features.
/~features/
/ /~step-definitions/
/ / /-steps.rb
/ /-example1.feature
/ /-example2.feature
/ /~support/
/ / /-env.rb/
This is fine for a small project, but let’s say you get above 10-15 scenarios, soon the steps file will become very big and you will start spreading steps over a few files with roughly representative names. Then you run headfirst into the ‘Global Scope Problem’….
Cucmber has the concept of a ‘World’ object, a kind of giant mixin which allows your Features, Steps and Support files to share context through the use of Ruby instance variables (the ones preceded by @ characters). Whilst this is essential to the functioning of Cucumber it is also our enemy.
As you’re project grows this shared scope can cause chaos, an IDE or editor that understands Cucumber syntax will help, but ultimately you will have features mapping to steps which could be in any steps file and where state can be affected by any other step in any file via instance variables.
This can end up as mess of procedural spaghetti.
The Page Object solution and the MGF Pattern
An excellent blog post by Joseph Wilk got me thinking seriously about this when I was working on a project which was being bitten hard by the global scope problem. Integrating the Page Object Pattern into your Cucumber tests is really your only option for containing the problem of global scope.
At this point I am going to introduce what I am calling the MGF pattern, which stands for ‘Model, Glue, Feature’.
- Model - You’re page objects
- Glue - The step definitions
- Feature - The Gherkin Features
The Page Object Pattern will be familiar to most of those reading this blog so I won’t go over it again. However I will suggest the following ‘Golden Rules’. I have adapted these from Simon Stewart’s wiki page on the Selenium site because as with most things he has it nailed:
- PageObject methods return other PageObjects or Self (whenever a page load is triggered)…
- Assertions should nearly always be in the Glue (step defs) not Page Objects (exception - ‘correct location’ check)
- Different results for the same action are different methods (‘save_expecting_error’, ‘save_expecting_submit’)
- Never call steps from steps (add helpers to objects if compounding steps are needed)
- Have a hash of element locators in each PageObject (or register custom locators with Capybara)
- NEVER pollute test code with page internals i.e. no css xpath in test code
Page objects possess all the knowledge about the application under test, the Glue simply binds the objects to the Gherkin features.
If you follow this pattern I would expect you to end up with a project layout something like that below.
/~features/
/ /~examples/
/ / /~step-definitions/
/ / / /-steps.rb
/ / /-signin.feature
/ / /-dashboard.feature
/ /~support/
/ / /-env.rb/
/ / /~objects/
/ / / /-signin.rb
/ / / /-dashboard.rb
Intergrating Capybara
Many people including myself like using Capybara and so it is worth noting how to integrate this into your Page Objects. Because Page Objects will live outside the Cucumber ‘World’ then you have to options available:
- Include the Capybara::DSL module in you’re page objects
- Pass around an instance of a Capybara Session
The advantage of the first method is that you only have to do this in a ‘base’ class which all other objects can inherit from and obtain those methods, the advantage of the second method is that you could potentially pass in a mock if you wanted to unit test you’re page objects (though the value of doing this is debatable and could fill an entire blog post).
Below is an example base class page object which may help get you started. In this example @session is an instance of an object which acts as a container for our current page and is the only variable that couples the step definitions (or Glue) it also acts as a way of storing data that needs to persist across a test run e.g. user.
class GenericPage
include Capybara::DSL
include Capybara::Node::Matchers
include RSpec::Matchers
attr_accessor :url
def initialize(session)
@session = session
end
def correct_page?
page.should have_css self.class::ELEMENTS['page check element']
end
def element_exists?(element)
page.has_selector? self.class::ELEMENTS[element]
end
def current_url
page.current_url
end
end
class SigninPage < GenericPage
LOCATION = 'http://myapp.com/signin'
ELEMENTS = {
'page check element' => '.myapp-signin > h1',
'page check value' => 'Weclcome to MyApp',
'username' => '#username',
'password' => '#password',
'save' => '#submit_button',
}
def initialize(session)
super
end
def visit
Capybara::visit LOCATION
end
def signin_expecting_success
signin
DashboardPage.new
end
def signin_expecting_error_using(unique)
signin
SigninErrorPage.new
end
private
def signin
fill_in ELEMENTS['username'], :with => @session.user.username
fill_in ELEMENTS['password'], :with => @session.user.password
click_on ELEMENTS['save']
end
end
Conclusions
Adding Page Objects and following the MGF pattern will help you combat the ‘Global Scope’ problem that can hit on large projects using Cucumber to drive automated tests. It will mean changes can be isolated to single points in the code and add logical structure which will reduce the maintenance burden.
Update…
I have just finshed reading some excellent posts on Nathaniel Ritmeyer’s blog which elaborate on how we can manage page objects thus tackling the issues of step coupling. He also maintains SitePrism which looks like an interesting dsl for page objects (and removes the need for the ugly constants I have in the examples above).
Anyway I think I will follow up this post in a couple of months and elaborate on the issues around coupling in our glue code and how best we can minimize what is one of the major problems with Cucumber.