I'm a big fan of page object pattern used for developing Selenium tests and I like the whole approach it follows. However, there are some points which can be improved in this approach. Let's talk about them.
Imagine that we have profile page for authenticated user. It has navigation menu with a set of links: "Home", "Profile", "Messages". Typical page class will look like this:
Home and Messages pages are rather different page, but still have navigation menu. So they should have the same set of methods. We shouldn't duplicate code, so we need to extract these methods into a separate module.
That's very good. But let's imagine that we begin to write tests for admin user. He certainly has profile page with navigation menu, but it contains different links: "Add user", "Content". The first idea is to add few more methods to NavigationMenuRegion. We won't care because simple authenticated user won't touch them. This will look like this.
But what if we have 5 roles, each with a custom set of links? And same menu is shown to anonymous user? This will bring a mess into a module. Then, let's imagine that we several regions with elements depending on role. This may become a nightmare for tester to support.
But what if our page class will check the currently logged in user's role and provide page object with a set of methods unique for it?
Let's imagine that each page has small link with username in header (obvious) and we follow a rule of thumb to name user with his role. Then we can figure out the role of currently logged in user with something like this (in our base page class):
I understand that we can get role via other ways (e.g. if we use fixtures or have access to application code), but let's use username link just for example.
So, now we can split NavigationMenuRegion into a set of role-based modules.
Ok, but as far it doesn't change anything because we didn't add user role check. And there goes Ruby magic!
Pretty easy, ain't it? I really like methods like #extend in Ruby, because when it's called for class, it adds class methods to it, but when it's called for object - it adds instance methods to it. However, they make code so elegant!
The key of it is that we overwrite #initialize thus ensuring it's called on page object creation. We should also call super in case page class has custom #initialize.
I cater this pattern for a month and it shows itself really good. We keep our code organized, page objects are flexible and easy to support.
I'm sure I'm not the first who came to this, but I didn't manage to google anything like this.
P.S. I called it "polymorphous" because page objects change themselves depending on environment. Maybe it's not a good title, but whatever.
Imagine that we have profile page for authenticated user. It has navigation menu with a set of links: "Home", "Profile", "Messages". Typical page class will look like this:
class ProfilePage def click_home_link @browser.find_element(:id => 'home_link').click HomePage.new end def click_profile_link @browser.find_element(:id => 'profile_link').click ProfilePage.new end def click_messages_link @browser.find_element(:id => 'message_link').click MessagesPage.new end end # ProfilePage
Home and Messages pages are rather different page, but still have navigation menu. So they should have the same set of methods. We shouldn't duplicate code, so we need to extract these methods into a separate module.
module NavigationMenuRegion def click_home_link @browser.find_element(:id => 'home_link').click HomePage.new end def click_profile_link @browser.find_element(:id => 'profile_link').click ProfilePage.new end def click_messages_link @browser.find_element(:id => 'message_link').click MessagesPage.new end end # NavigationMenuRegion class ProfilePage include NavigationMenuRegion end # ProfilePage class HomePage include NavigationMenuRegion end # HomePage class MessagesPage include NavigationMenuRegion end # MessagesPage
That's very good. But let's imagine that we begin to write tests for admin user. He certainly has profile page with navigation menu, but it contains different links: "Add user", "Content". The first idea is to add few more methods to NavigationMenuRegion. We won't care because simple authenticated user won't touch them. This will look like this.
module NavigationMenuRegion def click_home_link @browser.find_element(:id => 'home_link').click HomePage.new end def click_profile_link @browser.find_element(:id => 'profile_link').click ProfilePage.new end def click_messages_link @browser.find_element(:id => 'message_link').click MessagesPage.new end def click_add_user_link @browser.find_element(:id => 'add_user_link').click AddUserPage.new end def click_content_link @browser.find_element(:id => 'content_link').click ContentPage.new end end # NavigationMenuRegion class ProfilePage include NavigationMenuRegion end # ProfilePage class HomePage include NavigationMenuRegion end # HomePage class MessagesPage include NavigationMenuRegion end # MessagesPage
But what if we have 5 roles, each with a custom set of links? And same menu is shown to anonymous user? This will bring a mess into a module. Then, let's imagine that we several regions with elements depending on role. This may become a nightmare for tester to support.
But what if our page class will check the currently logged in user's role and provide page object with a set of methods unique for it?
Let's imagine that each page has small link with username in header (obvious) and we follow a rule of thumb to name user with his role. Then we can figure out the role of currently logged in user with something like this (in our base page class):
class Page def user_name @browser.find_element(:id => 'username_link').text end def user_role case self.user_name when /simple/ then :simple when /reader/ then :reader when /writer/ then :writer when /editor/ then :editor when /admin/ then :admin else :anonymous end end end # Page
I understand that we can get role via other ways (e.g. if we use fixtures or have access to application code), but let's use username link just for example.
So, now we can split NavigationMenuRegion into a set of role-based modules.
module NavigationMenuRegion module Simple def click_home_link @browser.find_element(:id => 'home_link').click HomePage.new end def click_profile_link @browser.find_element(:id => 'profile_link').click ProfilePage.new end def click_messages_link @browser.find_element(:id => 'message_link').click MessagesPage.new end end # Simple module Admin def click_add_user_link @browser.find_element(:id => 'add_user_link').click AddUserPage.new end def click_content_link @browser.find_element(:id => 'content_link').click ContentPage.new end end # Admin end # NavigationMenuRegion
Ok, but as far it doesn't change anything because we didn't add user role check. And there goes Ruby magic!
module NavigationMenuRegion # # Checks current user role and extends page object with # corresponding module. # def initialize super extend case self.user_role when :simple then Simple when :admin then Admin else Default end end module Simple # there go our methods for simple user end # Simple module Admin # there go our methods for admin user end # Admin module Default # we can add as many modules as we need, but let's have default end # Default end # NavigationMenuRegion
Pretty easy, ain't it? I really like methods like #extend in Ruby, because when it's called for class, it adds class methods to it, but when it's called for object - it adds instance methods to it. However, they make code so elegant!
The key of it is that we overwrite #initialize thus ensuring it's called on page object creation. We should also call super in case page class has custom #initialize.
I cater this pattern for a month and it shows itself really good. We keep our code organized, page objects are flexible and easy to support.
I'm sure I'm not the first who came to this, but I didn't manage to google anything like this.
P.S. I called it "polymorphous" because page objects change themselves depending on environment. Maybe it's not a good title, but whatever.
4 comments:
Can you show the bit where you're defining @browser and how you keep from having multiple browsers open each time you access a new page. When I set up my system to try and do something similar to your code below, ProfilePage.new opens a new browser instead of using an existing @browser that I've already setup.
def click_profile_link
@browser.find_element(:id => 'profile_link').click
ProfilePage.new
Thanks for your help.
My examples are incorrect in that because I used different approach in general. Instead of @browser I had #browser method which returned class attribute like. Something like in https://gist.github.com/1717569
Hi Alex, this is a nice idea and clean demonstration code.
We have built similar logic in our internal page models gem at Expedia several years back. I found it can be quite difficult to debug, should a test fail in such a structure. Dynamic programming defining behaviour and structure of your objects depends on some run-time conditions, which can be brittle and hard to trace. E.g. inability to simply jump-to-source to track the failure down is just tip of an iceberg.
I was struggling to define a good trade-of balance between complexity and re-usability in the test framework. Imagine, in addition to different user roles you've got app customisation across dozens of Points of Sales, and ever going AB tests in place ;-) You may end up making your test system as complex as your application under test but obviously much more brittle and expecting a lot of maintenance efforts.
I am trying to take simpler approach and chunk tests by test objectives, and in many cases buy simplicity over re-usability.
Would be great to hear your thoughts.
Cheers,
Alex
thanks for sharing a nice blog keep posting https://snowflakemasters.in/
Post a Comment