29

With Test Automation's Page Object Model we link pages together like this:

WebDriver driver = new WebDriver()
HomePage homePage = new HomePage(driver);
LoginPage loginPage = homePage.GoToLoginPage();
WelcomePage welcomePage = loginPage.Login();
etc
etc

The big benefit of this is if the Devs change the homepage so it no longer links to the loginpage, I can update my homepage class and see all the tests I need to update (with errors) before even running a test.

With Gherkin however, each row above would form a separate 'Step' and therefore a separate method. Therefore, how can this linking be done?

Is the only way to place instances of the page object classes (e.g. homePage, loginPage, etc) into a cross gherkin statement persistant store (e.g. like a specflow POCO or 'World')?

Greg Burghardt
  • 17,900
  • 9
  • 49
  • 92
Charlie S
  • 4,366
  • 6
  • 59
  • 97
  • 1
    Maybe this good [post](http://thomassundberg.wordpress.com/2011/10/18/testing-a-web-application-with-selenium-2/) can help you. – troig Oct 16 '14 at 08:11
  • That is a great post. Only slight concern is the instances of page objects passed between steps would not remain if multiple step classes were used. Using worlds / ootb dependency injection resolves this but code gets quite wordy having pageWorld infront of every page object instance. – Charlie S Mar 30 '15 at 09:49
  • 1
    Currently 23 upvotes to this question (high for a test automation question) but only 4 votes for the top answer. This hints to me that Page Object Model is NOT particularly compatible with Gherkin... a fear that has been increasing the more I use the 2 approaches together. I can't help but feel the main blame lies with Cucumber since it puts a very strange structure ontop of a tried and tested Class/Method structure. – Charlie S Jun 12 '19 at 12:11

5 Answers5

8

Ok so having asked numerous dev and test automation experts, it seems the solution is to continue with linking [e.g. WelcomePage welcomePage = loginPage.loginWithValidUser(validUser)] is the way to go.

To persist instance of page objects across steps (e.g. welcomePage in example above) you can use dependency injection tool (creating functionality similar to World extensions in Ruby's implementation of cucumber).

Here is more info: https://cukes.info/docs/reference/java-di

However, most projects will benefit from a Dependency Injection module to organize your code better and to share state between Step Definitions.

More info from SpecFlow (the .net official cucumber implementation):

http://specflow.org/getting-started/beyond-the-basics/

And finally, I have created a whole blog around this area that might help people out, since gherkin/page object interaction is a subject of great interest to me:

http://www.seligmanventures.com/dev-blog/test-automation-page-object-model-with-gherkin

Charlie S
  • 4,366
  • 6
  • 59
  • 97
  • 1
    I have done what you describe before and I actually would not recommend it. It does give you compile errors when you change the navigation, but the problem is that your are mixing an action (eg login) with an assertion about what happens ('I end up on the welcome page'). This makes it ugly to test things like what happens when you get your password wrong - you end up having to do things like LoginPage loginPage = loginPage.loginButExpectError(). IT just gets increasingly messy. – Perryn Fowler Feb 24 '17 at 09:47
  • 1
    These days what I do instead, is I seperate the action, and the assertion about where I should be when the action is complete. Something like loginPage.login(); WelcomePage welcomePage = onPage(WelcomePage.class); – Perryn Fowler Feb 24 '17 at 09:48
  • Perryn I hear what you are saying about invalid logins, but that is simply overcome by adding extra methods (e.g. loginWithValidUser() and loginWithInvalidUser()). Overall the benefit of chaining outweighs the extra methods required. Besides the code sitting behind both methods can be extrapolated out as a private method that sits quietly out of the way in the page object. – Charlie S Oct 04 '17 at 08:31
  • After much debate on this topic, I have just read these 2 official SpecFlow documents which seem to tend towards this answer, but not definitively: https://specflow.org/documentation/Sharing-Data-between-Bindings/ https://specflow.org/documentation/ScenarioContext/ – Charlie S Nov 07 '19 at 09:55
  • Interestingly this question has more upvotes than its top answer (by quite a way). This is very unusual on stackoverflow. I guess its shows that Gherkin and POM are probably not particularly compatible. – Charlie S Jan 29 '21 at 17:23
0

When it comes to most websites (where url's can be used), in my opinion it is best practice to simply use the url instead of an action to get to that same url.

For instance:

# Suggested by OP:
driver = Selenium::Webdriver.for :chrome, prefs: prefs
homepage = Homepage.new(driver)
login = homepage.go_to_login
welcome = login.log_in_as('dave4429')

# My Suggestion:
homepage = Url.new('/')
login = Url.new('/login')
welcome = Url.new('/welcome')

This means that you start from a url instead of having to start at the homepage for every test. You would still have the methods that you suggested, but they would be used in other areas, in order to make sure that the user can access the page through means other than the url.

However, this is not a one stop shop solution. With mobile and desktop applications, your only option may be to go through the home screen, in which case, the method you suggested is definitely the one to go for.

"Page objects themselves should never make verifications or assertions. This is part of your test and should always be within the test’s code, never in an page object." - Selenium HQ

The example I gave was a very basic one, and I would most likely wrap these into modules and classes, to enable coding like this:

google = Project::Pages::Google.new

google.search_for('Hello, World!')
expect(google.found_result?).to_equal(true)

Edit

In addition to this, you seem to have a misconception about how Cucumber works with Gherkin.

You can have multiple lines of code per step, as the step itself is a description of the actions within the step.

For instance:

Given I am logged in as "dave4429"
When I have submitted the "Contact Us" form with the following data:
   | dave4429@example.com | David McBlaine | I want to find out more about your Data Protection services, can I talk to a staff member or get a PDF? |
Then an email should be sent to "support@example.com" with the details specified

The definition for the "When" may look like this:

When(/^I have submitted the "Contact Us" form with the following data:$/) do |table|
  rows = table.raw
  row = rows[0]

  contact_us.fill_form({email: row[0], username: row[1], message: row[2]})
  contact_us.submit_message
  expect(browser.title).to_equal("Message Sent!")
end

It all depends on how much you break down the steps within the definition.

Edit #2

It's also clear to me that you want to do method chaining, something in the way of contact_us.fill_form({email: row[0], username: row[1], message: row[2]}).submit_message, which again, isn't out of the question while using the techniques that I'm suggesting, but the question of whether this chaining should be for each individual page, or whether everything should be included in one class or module, can only be answered by your needs.

It's just my opinion that this would put too much into a single class, and that breaking down that class will allow for more control to be given to the testers, and less redundant code will be written.

KyleFairns
  • 2,947
  • 1
  • 15
  • 35
0

Another option I have seen recently would be to store the Page Object instances as Static variables that can be accessed from any class?

Charlie S
  • 4,366
  • 6
  • 59
  • 97
  • Using statics would allow testers to go to any page of the AUT, even if that is not the behaviour of the AUT. Using instances forces you to only writes tests that reflect the actually behaviour of the AUT – Charlie S Feb 25 '20 at 11:06
-1

After much discussion on this topic, an equally plausible alternative is to not return instances of new pages when using the page object pattern with gherkin. You will lose the benefit of linking that you normally get with POM, but the code will arguably read better and be less complex. Posting this alternative answer, so as a test community we can vote which method is peoples preference.

Charlie S
  • 4,366
  • 6
  • 59
  • 97
  • Feel -2 is a bit harsh for this answer. It is not my preferred answer, but is definitely a viable alternative and does reduce the complexity. Please comment on reason for minus points as may help to produce an 'improved' answer – Charlie S Oct 13 '17 at 10:53
  • Here is an example of not storing the page objects returned by navigations through pages: https://www.testautomationtribe.com/specflow-with-page-object – Charlie S Feb 07 '18 at 08:38
  • Was really liking this approach, especially after it was pointed out to me that gherkin steps are coded as independent entities and therefore creating instances of the relevant page object at the start of the step fitted semantically very nicely with that. However, just realised after trying to implement this approach, that I think you would need to keep the driver instance exposed at the step level (rather than keeping it encapsulated within the page objects and passed around as you navigate between them). Is there a way round this anyone can think of? – Charlie S Mar 06 '18 at 09:21
-1

This can be a bit tricky with Cucumber and Selenium. I've developed a pattern that involves extension methods to the IWebDriver interface for Selenium allowing me to navigate to specific pages using the page objects. I register the IWebDriver object with the SpecFlow dependency injection framework, and then my step definition classes are free to initialize whichever page objects they need.

Registering Selenium Web Driver With SpecFlow

You just need to plug in to the before/after scenario hooks to manage the web driver object:

[Binding]
public class WebDriverFactory
{
    private readonly IObjectContainer container;

    public WebDriverFactory(IObjectContainer container)
    {
        this.container = container;
    }

    [BeforeScenario]
    public void CreateWebDriver()
    {
        var driver = new ChromeDriver(...);

        // Configure Chrome

        container.RegisterInstanceAs<IWebDriver>(driver);
    }

    [AfterScenario]
    public void DestroyWebDriver()
    {
        var driver = container.Resolve<IWebDriver>();

        if (driver == null)
            return;

        // Capture screenshot if you want
        // var photographer = (ITakeScreenshot)driver;

        driver.Quit();
        driver.Dispose();
    }
}

Then It's a matter of gluing step definitions and page objects together using some extensions on the IWebDriver interface.

Selenium Page Objects

Keep your page objects navigating to one another. For instance the HomePage allows you to navigate to the "Create blog post" page, and returns the page object for that page:

public class HomePage
{
    private readonly IWebDriver driver;
    private readonly WebDriverWait wait;

    private IWebElement CreatePostLink => driver.FindElement(By.LinkText("Create New Blog Post"));

    public HomePage(IWebDriver driver)
    {
        this.driver = driver;
        wait = new WebDriverWait(driver, 30);
    }

    public AddEditBlogPostPage ClickCreatePostLink()
    {
        CreatePostLink.Click();
        wait.Until(d => d.Title.Contains("Create new blog post"));

        return new AddEditBlogPostPage(driver);
    }
}

And subsequently, the AddEditBlogPostPage returns the BlogPostListingPage when you create a new blog post:

public class AddEditBlogPostPage
{
    private readonly IWebDriver driver;

    private IWebElement Title => driver.FindElement(By.Id("Title"));
    private IWebElement PostDate => driver.FindElement(By.Id("Date"));
    private IWebElement Body => driver.FindElement(By.Id("BodyText"));
    private IWebElement SaveButton => driver.FindElement(By.XPath("//button[contains(., 'Save Blog Post')]"));

    public AddEditBlogPostPage(IWebDriver driver)
    {
        this.driver = driver;
    }

    public BlogPostListingPage CreateBlogPost(BlogPostDataRow data)
    {
        Title.SendKeys(data.Title);
        PostDate.SendKeys(data.Date.ToShortDateString());
        Body.SendKeys(data.Body);
        SaveButton.Click();

        return new BlogPostListingPage(driver);
    }
}

Step Definitions To Glue Things Together

The step:

When I create a new blog post:
    | Field | Value                              |
    | Title | Selenium Page Objects and Cucumber |
    | Date  | 11/1/2019                          |
    | Body  | ...                                |

Would have this definition:

[Binding]
public class BlogPostSteps
{
    private readonly IWebDriver driver;

    public BlogPostSteps(IWebDriver driver)
    {
        this.driver = driver;
    }

    [When(@"I add a new blog post:")]
    public GivenIAmAddingANewBlogPost(Table table)
    {
        var addBlogPostPage = driver.GoToCreateBlogPostPage();
        var blogPostData = table.CreateInstance<BlogPostDataRow>();

        addBlogPostPage.CreateBlogPost(blogPostData);
    }
}

The driver.GoToCreateBlogPostPage(); is an extension method on IWebDriver that kicks off the navigation from one page object to another:

public static class SeleniumPageNavigationExtensions
{
    public static AddEditBlogPostPage GoToCreateBlogPostPage(this IWebDriver driver)
    {
        var homePage = new HomePage(driver);

        return homePage.ClickCreatePostLink();
    }
}

This gives you the flexibility to keep your page objects "pure" and devoid of SpecFlow, Cucumber and Gherkin. You can use these same extension methods and page objects in other tests that do not utilize Gherkin or behavior driven development. This allows for easy reuse of your test classes. Your test projects should be just as purposefully architected as the actual application it tests.

Greg Burghardt
  • 17,900
  • 9
  • 49
  • 92