The page object model works very well, but there are a few traps you can fall into. Alister Scott goes through a few of them and mentions one that particularly grates me: “Pages stored as instance variables”. Here’s a demonstration of the problem:
So why is that a problem? Well, there’s lots of noise – 3 lines are there just to create variables. This significantly reduces readability as you’ll end up with many, many lines of test code just creating instances of page objects. You also now have a whole load of instance variables to keep track of: @login_page, @account_page, @account_history_page. And when you’re using cucumber to run your tests, this will lead to *big* maintenance issues. I’ve had to rescue a few cucumber-based test projects and one of the most frequent causes for test-rot is that the testers lost track of their instance variables. Been in this situation before?
“Can I use @account_page here? Did I previously declare it? Hmmm… No, it’s nil when I try to use it. OK, I’ll instantiate it here. [runs the tests]. Cool, that works. Oh no! Doing that has broken some other tests that referenced @account_page but expected something else!?!? Should I fix up the other tests? Rename the @account_page variable to something else? If I do that will I break anything else?” Not fun. Big spaghetti problems.
But what to do about it? Alister suggests using blocks (provided by the page-object gem) that look something like the following:
On first glance, this looks great. No instance variables to keep track of. Just deal with the classes themselves and use only local variables inside the blocks. Nothing to keep track of. Great!
But…
The above proposal causes other maintenance hassles – the page object’s class name is now scattered throughout the code. Lots of “visit LoginPage” all over the place. What happens when the class name changes? You’ll have to make changes throughout the code. Not fun.
Solution: use an instance of an App class, this App class being a representation of the app you’re testing (the whole app, not individual pages). This App class contains one method per page class, each of these methods return an instance of the relevant page class.
“Whhhaaattt?”
OK, here’s an example:
In your tests, you would then have the following line:
When I visit the login page
…which would match the following step…
If you structure your tests such that they always begin by mentioning where the user starts (a good idea as it gives context), you can rely on the fact that @app has been instantiated so you can just use it. For example:
So how is this an improvement? Well, there’s no need to manage instance variables for different pages – just call methods on a known instantiation of the App class(@app) and they’ll return instances of the pages you want. There’s no need to mention class names; they are hidden behind methods. If the class name changes you only need to make one change (change the class referenced in the methods in the App class).
Subjective statement: I’d also argue that you also get great readability with this way of structuring things.
This is how I’ve normally organised things. And it’s worked great both on small projects of only 10′s of tests to large projects where the number of tests is 1000+. It’s the best solution I’ve come up with, it doesn’t suffer from having instance variables all over the place, neither are there class names all over the place.
I’ve written up in brief how this works if you’re using SitePrism to manage your page objects: http://rdoc.info/gems/site_prism/file/README.md#Epilogue
I could argue that maintaining an App class with mappings from method names to class names is just as much as a maintenance overhead (if not more!) than having class names in your steps.
For example, say you rename your HomePage class to MainPage. As you suggest, you could just change the name in your App class to MainPage, but then your home method would be calling MainPage, so I would prefer to rename home to main also, which causes the same problem you mentioned in updating multiple steps. Otherwise you start building up mismatches between your app method name and your class names, and having hundreds of pages is bound to cause confusion!
Plus, I use RubyMine so I like being able to command-click the class name directly from the steps so I can see the page class directly (without going through an App class).
As per your example that visits multiple pages: I don’t belive you even need to use an app instance variable, or case statement, you could just dynamically call the appropriate method in one line:
When /^I visit the (home|login|account|account history) page$/ do |page_name|
visit App.new.send page_name
end
instead of
When /^I visit the (home|login|account|account history) page$/ do |page_name|
@app = App.new # <= the only place where @app is instantiated
target_page = case page_name
when 'home' then @app.home
when 'login' then @app.login
when 'account' then @app.account
when 'account history' then @app.account_history
end
visit target_page
end
Thanks for the feedback Alister! I’ll answer your points in turn:
1) Regarding maintaining an App class, from experience this file has one of the lowest number of checkins in a project. To give an example, the project I’m currently working on (of around 500 tests) has changed 8 times. And all but two of the changes are minor, eg: adding pages. The other 2 changes are when we’ve had to change the page object class name. BTW, this is very similar to other cuke projects I’ve worked on – the reality is that this setup really doesn’t require much maintenance at all.
1a) “Otherwise you start building up mismatches between your app method name and your class names” – again, sounds reasonable, but in reality this doesn’t seem to happen – certainly not on any project I’ve been on. If I were to just change class names for fun then I’d definitely end up in the situation you describe, but page object class names don’t change too much.
I use this structure to deal with different page designs when they are tried – for example an A/B testing effort. Eg: design 1 of an account page may be superseded by a completely different design 2. Though the page shows the same things, the DOM structure is completely different to the point where the selectors in the page object model are completely different. The same elements are there on the page, they’re just in completely different places. So I create a new page object model that contains the same elements, just different selectors. The interface is the same so I can just swap the new one in place of the old… and the tests still work! Renaming the App’s method name isn’t necessary, everything still works, but we’re using a different page object.
So yeah, mismatch is possible, but the scenario that would create that problem doesn’t happen for me. But I see your point – you could easily end up there. And then you’re in no better position than using page object class names all over the place.
2) “I use RubyMine” – I use MacVim so sadly your suggestion doesn’t help me
But vim is awesome…
3) “you could just dynamically call the appropriate method in one line” – you give the following example:
visit App.new.send page_nameI added the “account history” example just to ward off this suggestion – it wouldn’t work. I’ve tried it before but there are too many scenarios where it doesn’t work. I ended up with a whole load of magic to get around various issues but in the end decided to go with a simple case statement – sometimes the easiest solutions are the best!
Brilliant !
Great post Nat….really well thought out, and site_prism looks equally well founded…nice work!
I have worked on some big Cuke projects as well and have been battling the ‘global scope’ problem as well, as described in this scratched out blog post:
http://opensourcetester.co.uk/2012/08/06/cucumber-page-object-global/
I was keen to follow Simon Stewart’s ideas where all actions that have different outcomes are modeled as different methods and anything that can cause a page load should return a new page object.
I ended up with the concept of a @journey or @session which holds a reference to a ‘current_page’, your steps then don’t have to worry about any type information e.g.
@journey.current_page = @journey.current_page.submit_expecting_error_page
This brings it’s own problems of course as you’re steps are now a bit magical…but it does limit coupling between steps….
I fear really that this is a big issue with scaling up Cucumber tests!!!
I use a similar pattern, but call it a “Site” object instead of “App”.
I use the Site object as a factory for all pages on a site which gives it something like a namespace (iif you write tests that span more than 1 site you’re likely to have multiple login pages.) The site also ensures that only 1 instance of a page exists at a time.
class MySite
def initialize(browser, config)
@browser = browser
@url_base = config['url_base']
end
def LoginPage
@login_page || = LoginPage.new(@base_url)
end
end