Is Page Object Model linking compatible with Cucumber's Gherkin?
Asked Answered
G

5

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')?

Gorlin answered 15/10, 2014 at 9:19 Comment(3)
Maybe this good post can help you.Bassorilievo
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.Gorlin
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.Gorlin
G
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

Gorlin answered 30/3, 2015 at 12:16 Comment(5)
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.Lobation
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);Lobation
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.Gorlin
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: specflow.org/documentation/Sharing-Data-between-Bindings specflow.org/documentation/ScenarioContextGorlin
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.Gorlin
E
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:
   | [email protected] | 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 "[email protected]" 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.

Escaut answered 5/10, 2017 at 12:55 Comment(0)
G
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?

Gorlin answered 31/10, 2019 at 17:31 Comment(1)
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 AUTGorlin
G
-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.

Gorlin answered 4/10, 2017 at 15:29 Comment(3)
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' answerGorlin
Here is an example of not storing the page objects returned by navigations through pages: testautomationtribe.com/specflow-with-page-objectGorlin
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?Gorlin
S
-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.

Schizopod answered 1/11, 2019 at 15:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.