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