3

I am wondering what the best way to structure context injection is. I know that page objects should be instantiated / disposed of as they are needed (don't initialize all page objs at the start of a test). I am wondering what the best way of passing a page object around between tests is.


Approach one:
Every time you navigate to the new page, just add it to the scenario context

 [Given(@"I am on the homepage"]
 public void GivenIAmOnTheHomepage(){
     ScenarioContext.Current.Set(new Homepage().NavigateTo());
 }

 [Then(@"The homepage should have a banner"]
 public void ThenTheHomepageShouldHaveABanner(){
    var homepage = ScenarioContext.Current.Get<Homepage>();
    Assert.IsTrue(homepage.BannerIsDisplayed());
 }     

Con:
If you navigate to another page, the homepage isn't disposed

 [When(@"I click on the FAQ link")]
 public void WhenIClickOnTheFAQLink(){
    var homepage = ScenarioContext.Current.Get<Homepage>();

    FAQPage FAQ = homepage.ClickFAQLink();
    ScenarioContext.Current.Set(FAQ);

    //homepage is not disposed, even though we are no longer on the homepage!
 }


Approach two: Store the current page object using a key, use that key to access it every time

 [Given(@"I am on the homepage"]
 public void GivenIAmOnTheHomepage(){
     ScenarioContext.Current.Set(new Homepage().NavigateTo(), "currentPage");
 }

 [Then(@"The homepage should have a banner"]
 public void ThenTheHomepageShouldHaveABanner(){
    var homepage = (Homepage)test["currentPage"];
    Assert.IsTrue(homepage.BannerIsDisplayed());
 }

 [When(@"I click on the FAQ link")]
 public void WhenIClickOnTheFAQLink(){
    var homepage = (Homepage)test["currentPage"];

    FAQPage FAQ = homepage.ClickFAQLink();
    ScenarioContext.Current.Set(FAQ, "currentPage");

    //homepage is disposed
 }

Con:
This is more likely to have typos, and if I want to pass more than just page objects around (like the current customer that we are using, or a promotion code) the number of keys can add up and get confusing. Also, casting the current page every time you want to access it feels messy.


Anyone have any recommendations for this?

1

I use Spinach and write tests in Ruby but I face the same issues when writing my tests - looks like managing page object context across the steps is a common problem when using Cucumber-like BDD frameworks. I think one can hardly find a perfect and universal solution for this one. All of them have their pros and cons. I tried several approaches (the code snippets are in Ruby but the logic behind is very much the same):

Approach 1: current_page approach (which you also described) seems to be widely used. The main idea is to pass the page object context in the instance variable and change it in page objects when you call the method which navigates to another page:

step 'I am on the homepage' do
  @current_page = Pages::Homepage.new
  @current_page.should be_loaded
end

step 'I go to the contact page' do
  @current_page = @current_page.go_to_contact_page
end

step 'I should be on the contact page' do
  @current_page.should be_contact_page
end

And the context switching happens on the PageObject side:

def go_to_contact_page
  @session.find('.contact-page-link').click
  Pages::ContactPage.new #returning new contact page to the @current_page
end

I think this approach is quite elegant and is especially good for the wizard-like flows where you have to go through several steps (like purchasing an item, for example).

However, for applications with more complicated logic, especially when the amount of tests grows and you start to re-use some steps between several test scenarios to avoid code duplications you start facing some issues when in some step you expect one page object in @current_page but you get another one. Debugging and maintaining such tests becomes a real pain sometimes.

Approach 2: just initiate a new page object in every step and do not use the instance variables. In this case you always know which page object you are using in current step. This is especially important for shared steps (I don't know how it works in SpecFlow but in Spinach you have to store shared steps in separate files):

step 'I am on the homepage' do
  homepage = Pages::Homepage.new
  homepage.should be_loaded
end

step 'I go to the contact page' do
  homepage = Pages::Homepage.new
  homepage.go_to_contact_page
end

step 'I should be on the contact page' do
  contact_page = Pages::ContactPage.new
  contact_page.should be_loaded
end

As I mentioned, this looks more clear but at the same point too verbose. Also, in the tests with a lot of steps you will create a lot of objects in the memory which might be bad.

Approach 3: the one you said to be anti-pattern but I don't really think so: initiating all PageObjects at the beginning of the test. In Spinach this could be done using before hooks:

before do
  @home_page = Pages::HomePage.new
  @contact_page = Pages::ContactPage.new
end

step 'I am on the homepage' do
  @homepage.should be_loaded
end

step 'I go to the contact page' do
  @homepage.go_to_contact_page
end

step 'I should be on the contact page' do
  @contact_page.should be_loaded
end

In this case, tests are less verbose and you create less objects. This will not work if you need to pass some unique parameters when initializing the PageObject but from my experience you need this very rarely (and still, you can use a mix of two approaches in this case).

I understand that my answer is not providing a perfect solution for your problem but as I mentioned I don't believe there is a perfect and universal one. I think it will give some food for thought anyway. Looking forward to hear about this from others.

  • I think the general consensus is that #3 is an anti pattern, see this: sqa.stackexchange.com/questions/13570 – GKS1 Nov 5 '15 at 14:32
  • Maybe the best approach is number one, and have different methods for when you expect a different page object? ================= For example a Login() method could return an account page if it's successful, and a LoginWithWrongPassword() could return the error page or something like that. And both methods could call an EnterCredentials() method so we don't duplicate that part of the code. – GKS1 Nov 5 '15 at 14:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.