In this post I’ll focus on using page objects in a Selenium and Cucumber JVM context. Please note that this post follows on from the previous post, using the code you built up there.
If you did create everything from the last post, now would be a good time to rerun it and make sure it all works:
mvn clean test
I’m going to revise the example code from that previous post to use page objects. There are a lot of ways people structure all of this in Java. I’ll give you one approach here. If you followed along from the last post you definitely created a directory path of src/test/java. You may or may not have created a package structure within there. With my example, I had created a full path of src/test/java/com/testerstories.
Within that directory — or the one you created if you used a different package path — create a new directory called pages. This is where we’ll store our page objects. What we’re going to do is take the step definitions from the previous post and change them to use the page objects that we create. Let’s first consider what our step definition file (LoginSteps.java) looks like based on that previous work:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package com.testerstories; import static org.junit.Assert.assertEquals; import cucumber.api.PendingException; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import cucumber.api.java.After; import cucumber.api.java.Before; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; public class LoginSteps { WebDriver driver; LoginPage login; NavigationPage nav; @Given("^the Symbiote home page$") public void the_Symbiote_home_page() throws Throwable { driver.get("https://symbiote-app.herokuapp.com"); assertEquals("Symbiote", driver.getTitle()); } @When("^logging in as an admin$") public void logging_in_as_an_admin() throws Throwable { WebElement open = driver.findElement(By.id("open")); open.click(); WebElement username = driver.findElement(By.id("username")); username.sendKeys("admin"); WebElement password = driver.findElement(By.id("password")); password.sendKeys("admin"); WebElement login = driver.findElement(By.id("login-button")); login.submit(); } @Then("^the home page navigation is available$") public void the_home_page_navigation_is_available() throws Throwable { WebElement navList = (new WebDriverWait(driver, 10)) .until(ExpectedConditions.visibilityOfElementLocated(By.id("navlist"))); } @Before public void startUp() { driver = new FirefoxDriver(); } @After public void tearDown() { driver.quit(); } } |
If we consider that code, we’re using two pages as part of the logic. Or, rather, we’re using parts of pages. In this case, I’m using a login form and checking for a navigation list. Neither of these are an entire page but rather sections or areas within a page. This makes for a good point right off the top: a page object does not necessarily have to represent an entire page. I’m not going to cover the entire theory around page objects in this post. There’s plenty of material out there.
So first let’s create a LoginPage.java class in the pages
directory you just created. Put the following code in it:
1 2 3 4 |
package com.testerstories.pages; public class LoginPage { } |
Now create NavigationPage.java in that same location and put the following in it:
1 2 3 4 |
package com.testerstories.pages; public class NavigationPage { } |
I’m starting off kind of simple here just to show that page objects are nothing more than Java classes.
There a lot of different thoughts about how to construct page objects and how they should work. I’ll take the simple approach: I want to add methods on my page objects that essentially mimic actions that occur on those pages. So we’re going to start moving logic out of our LoginSteps and into our page objects.
If you go through the code, you’ll notice that the first thing I do in the LoginSteps is create an instance of the driver. This is done in a @Before method. Now, I could do that same thing in each page object so that when a page object is used, a browser is created. But, if I did that, it would mean that any time that page object is called, a new WebDriver instance is created. That doesn’t seem like a good idea. However, it is true that each page object is going to need an instance of the WebDriver so that it can run commands against it. But if that’s the case, which page do I put it in?
You don’t put it in one specific page. A good practice is that you create what some people call an abstract page or a base page. So in that pages directory, create a file called BasePage.java. Put the following in it:
1 2 3 4 |
package com.testerstories.pages; public class BasePage { } |
The first thing to do here is create an instance of WebDriver. But let’s keep something in mind: each of my page objects is going to inherit from this base page. That means I want each page instance to have access to the WebDriver instance. So let’s add this:
1 2 3 4 5 6 7 |
package com.testerstories.pages; import org.openqa.selenium.WebDriver; public class BasePage { protected WebDriver driver; } |
Here “protected” means any page inheriting from this page gets access to the driver variable. Now let’s create a constructor. We’re going to pass in the driver.
1 2 3 4 5 6 7 8 9 10 11 |
package com.testerstories.pages; import org.openqa.selenium.WebDriver; public class BasePage { protected WebDriver driver; public BasePage(WebDriver driver) { this.driver = driver; } } |
This simply sets up the driver variable so that it is associated with whatever driver is passed in. Note that the driver that is passed in must be of type WebDriver.
Now, here’s where approaches differ. One thing I want this base page to be able to do is navigate to the location of specific pages. At the very least, I need it to navigate to the login page. So let’s add the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.testerstories.pages; import org.openqa.selenium.WebDriver; import static org.junit.Assert.assertEquals; public class BasePage { protected WebDriver driver; public BasePage(WebDriver driver) { this.driver = driver; } public LoginPage navigateTo() { driver.navigate().to("https://symbiote-app.herokuapp.com"); assertEquals("Symbiote", driver.getTitle()); return new LoginPage(driver); } } |
Before getting into that, let’s go back to our @Given step in LoginSteps.java and change it like this:
1 2 3 4 5 6 7 8 9 10 |
... public class LoginSteps { @Given("^the Symbiote home page$") public void the_Symbiote_home_page() throws Throwable { // driver.get("https://symbiote-app.herokuapp.com"); // assertEquals("Symbiote", driver.getTitle()); } ... |
Notice the lines that I commented out are the ones I moved to the base page. I’m commenting these out rather than removing them so you can see this evolve. This is going to render our test useless, of course, but what we’re going to do is rebuild the test using the page object pattern. When done, we should be able to rerun the test and see it execute as it did before.
Now let’s consider line 17 in BasePage.java. What is that line actually doing? The contract of the page object model is to give you the ability to chain actions via the use of the page objects. How do I do that? Notice the navigateTo() method is set up to return a LoginPage object — i.e., an instance of the LoginPage class. But I need an instance of that page returned. That’s what line 17 is doing. Notice also that the driver instance is being passed to the newly created page object.
Now let’s go back to LoginPage.java and change it as such:
1 2 3 4 5 6 7 8 9 |
package com.testerstories.pages; import org.openqa.selenium.WebDriver; public class LoginPage extends BasePage { public LoginPage(WebDriver driver) { super(driver); } } |
Here I’ve extended the base page and added a constructor. What this is saying if I create a LoginPage and pass in a driver, I would call (via the super
method) the constructor of the BasePage, since I’m extending that class via this one.
Now looking at our next part of the test — the @When step — I have to log in to the site as an admin. What I want to do now is move the logic from that step into the page object. So I want to translate the act of logging in to a method on the page object. So let’s change LoginPage.java to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package com.testerstories.pages; import org.openqa.selenium.WebDriver; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; public class LoginPage extends BasePage { public LoginPage(WebDriver driver) { super(driver); } public LoginPage loginAsAdmin() { WebElement open = driver.findElement(By.id("open")); open.click(); WebElement username = driver.findElement(By.id("username")); username.sendKeys("admin"); WebElement password = driver.findElement(By.id("password")); password.sendKeys("admin"); WebElement login = driver.findElement(By.id("login-button")); login.submit(); return new LoginPage(driver); } } |
Notice how, once again, I return an instance of the LoginPage. Let’s once again change LoginSteps.java for the @When step as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
... public class LoginSteps { ... @When("^logging in as an admin$") public void logging_in_as_an_admin() throws Throwable { // WebElement open = driver.findElement(By.id("open")); // open.click(); // WebElement username = driver.findElement(By.id("username")); // username.sendKeys("admin"); // WebElement password = driver.findElement(By.id("password")); // password.sendKeys("admin"); // WebElement login = driver.findElement(By.id("login-button")); // login.submit(); } ... |
Again, all I’ve done is comment out the steps to show that those actions have now been moved to the page object.
The last test step, the @Then step, takes place after the login has occurred and checks for a navigation element that should be present. So let’s first update our NavigationPage.java similar to how we updated the LoginPage.java:
1 2 3 4 5 6 7 8 9 |
package com.testerstories.pages; import org.openqa.selenium.WebDriver; public class NavigationPage extends BasePage { public NavigationPage(WebDriver driver) { super(driver); } } |
This is just extending the BasePage class and putting in the constructor as I originally did with LoginPage. Now I want to take the action from the @Then step and create a method for that action. So let’s update the NavigationPage.java so it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package com.testerstories.pages; import org.openqa.selenium.WebDriver; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; public class NavigationPage extends BasePage { public NavigationPage(WebDriver driver) { super(driver); } public NavigationPage checkForNavigationList() { WebElement navList = (new WebDriverWait(driver, 10)) .until(ExpectedConditions.visibilityOfElementLocated(By.id("navlist"))); return new NavigationPage(driver); } } |
And, as before, let’s comment out what we no longer need in the @Then step in LoginSteps.java:
1 2 3 4 5 6 7 8 9 10 |
... public class LoginSteps { ... @Then("^the home page navigation is available$") public void the_home_page_navigation_is_available() throws Throwable { // WebElement navList = (new WebDriverWait(driver, 10)) // .until(ExpectedConditions.visibilityOfElementLocated(By.id("navlist"))); } ... |
So now we have a test that does nothing! Well, it does open a browser and then close it — due to the @Before and @After hooks — but there is no test logic. Everything has been moved to the page objects. But the page objects aren’t being referenced by the test steps. Let’s rectify that. Here is the full LoginSteps.java, with all no-longer-needed material — including imports — removed. Further, the test steps have been updated to use the page object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
package com.testerstories; import cucumber.api.PendingException; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import cucumber.api.java.After; import cucumber.api.java.Before; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; import com.testerstories.pages.LoginPage; import com.testerstories.pages.NavigationPage; public class LoginSteps { WebDriver driver; LoginPage login; NavigationPage nav; @Given("^the Symbiote home page$") public void the_Symbiote_home_page() throws Throwable { login = new LoginPage(driver); login.navigateTo(); } @When("^logging in as an admin$") public void logging_in_as_an_admin() throws Throwable { login.loginAsAdmin(); } @Then("^the home page navigation is available$") public void the_home_page_navigation_is_available() throws Throwable { nav = new NavigationPage(driver); nav.checkForNavigationList(); } @Before public void startUp() { driver = new FirefoxDriver(); } @After public void tearDown() { driver.quit(); } } |
On lines 17 to 19 I’m simply declaring the page variables so that I can use them in all of the steps. Then in each step I create (if necessary) a page object instances and references methods on that instance. As you can see, the test logic is quite a bit more concise because it is delegating everything to the page objects.
Try to run the test again and make sure it works:
mvn clean test
Some people, like myself, don’t like to distribute how elements are referenced throughout the page object logic. So you can modify LoginPage.java to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.testerstories.pages; import org.openqa.selenium.WebDriver; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; public class LoginPage extends BasePage { private final By openSelector = By.id("open"); private final By usernameSelector = By.id("username"); private final By passwordSelector = By.id("password"); private final By loginSelector = By.id("login-button"); public LoginPage(WebDriver driver) { super(driver); } public LoginPage loginAsAdmin() { WebElement open = driver.findElement(openSelector); WebElement username = driver.findElement(usernameSelector); WebElement password = driver.findElement(passwordSelector); WebElement login = driver.findElement(loginSelector); open.click(); username.sendKeys("admin"); password.sendKeys("admin"); login.submit(); return new LoginPage(driver); } } |
Spend a little time looking at the differences with this approach. I personally prefer something like this because it allows me to keep how the selectors for elements are defined isolated to one spot. I’ll take a moment here to admit that I’m a dynamic language person at heart and no matter how much cleanup you do, Java still provide way too much boilerplate for my liking. By contrast, consider a page definition in my Ruby-based Symbiont framework. The differences are like night and day.
Going back to the above approach, I should note that some folks like to use a PageFactory which can further clean up a page object, depending on your preferences. I’ll leave that as an exercise for the reader since it’s really just a slight variation on the theme.
The main goal for me here was to showcase a working example of the page object pattern but also to make sure you were aware that there are different thoughts on how to implement this pattern.
Even more importantly you see how to do this in a Java context, using Selenium and Cucumber JVM. This is an ecosystem that I’ve found some testers have a harder time getting involved, particularly if they have come from dynamic languages like Ruby or Python. Hopefully these few posts help you get started on using some of the more popular tools within the Java context for testing.
Hi Jeff
This is a super user friendly description of what needs to happen to get something up and running, with page objects. Great work.
I have just started to get my head around this stuff, and appreciate the detail you provide to explain why code lines are where they are.
I have a question……
suppose I want to have different step definition files, where I would keep common step definitions, what must I do to avoid null pointers with instances of my drivers. I have tried a base steps class, extending it for the steps definitions, but get null pointer on my driver when my glue code is in the other step file. I would really appreciate some guidance on this if you can spare the time.
Thank you again for the great tutorial.
Brian
Generally, so far as I know, as long as your step definitions are in a test resource folder they should be picked up. As far as getting null pointers for drivers, you might want to consider using a driver factory. I have two Java repos (although they don’t use Cucumber yet) that show what I mean.
There’s this one: https://github.com/jnyman/lucid-java-automation
If you look under src/test/java/com/testerstories/testing, you’ll see a config directory. In there I keep my driver instantiation so that I can have my pages (and my steps, if I was using Cucumber) reference the driver. You’ll see that repo in fact is set up to start accepting a Cucumber or jBehave like approach, in terms of separation into steps, pages, and so on.
I have another repo here: https://github.com/jnyman/java-automation-project
It’s pretty much the same thing but it was my first attempt. The second repo uses Maven while the first uses Gradle. All that being said, what works for me is to run my tests — either via Gradle or Maven — and as long as my entire structure is set up via the conventions, I shouldn’t get null pointers due to my drivers because of how the factory is used.
I’m going to be doing another iteration of this exact same framework in a little bit, where I do finally put Cucumber-JVM in front of these tests. I’m hoping to start working on that very soon (i.e., within a couple of days). I can ping you when that’s up because I will be doing something similar to what you are doing, I think, which is having a series of step definitions but then also some common ones (such as login and navigation).
As a further comment to my own comment: I ended up removing those existing Java repos that I mentioned. I did that because I’m very close to putting up a new Java repo that combines both of those and follows a few better practices.
I apologize for the confusion on this.
Hi Jeff
I really appreciate you replying. Yes please to keeping me informed about the progress of your tutorial project. Right now, I have a driver factory providing the driver to all my page objects. My new challenge is discovering the best way to handle page instances, where I have multiple step def files (basically a step def for each major page / tab). Again, I;m all ears to learn a good approach, and happy to talk off-line.
Looking forward to the next installment.
Kind regards
Brian
Thank you again for the great tutorial sharing with us.
Could you please add folder structure details ? (Screenshot)
~Anand Somani
This just uses the Maven default structure along with my own package convention. So the main source is:
I also added a
pages
package as well. So here is a simple visual of the structure:I’ve actually learned a bit more in terms of structure since I wrote this. In an upcoming post I’m going to point to a new repo on GitHub with a Java-based framework. Initially this will be without Cucumber in place but then a subsequent commit will include Cucumber, somewhat like what I did here.
Hi Jeff
Great work, very clear, easy to follow, easy to understand. Fantastic 5 A Start *****
Hi Jeff,
This is one of the best written technical blogs I have ever come across, I applaud you. I am learning how to implement a Page Object Pattern and this indefinitely helped. Thought I would comment just to say that :).
Zukky
Hello All,
Can anyone help me by sharing your POM framework done for Selenium Cucumber without Maven.
Please share in GitHub if you have a code and it would be of great help.
whatever you have written is rubbish regarding the page factory . it doesn’t work
As bug reports go, this one is unfortunately too much like what I see in my daily career. “It doesn’t work” isn’t all that indicative.
It is possible, of course, that something has changed in terms of how these elements work. I will put together a working example and see if I can find what made it “rubbish.”
Hi Jeff
Great guide.
Just wondering what approach you would take for my current situation.
I have a cucumber scenario which is basically “When I login with credentials, Then the login will be successful”. So for the “When” step we call a login() method which in turn returns a dashboard page and in the constructor of that dashboard page we check that the title is correct and the page has been loaded.
My question is what do we do in the “Then” step? Because really we’ve already checked that the dashboard page is displayed by calling the login() method (and returning a dashboard page) in the “When” step.
From what I can gather I have 3 options:
Leave the “Then” step def empty
Call “new DashboardPage(driver)” in the “Then” step def which does the same checks that were performed after called the login() method
Add an extra level of assertion in the “Then” step def which checks something different e.g. the correct heading is displayed
Would like to know your opinion on this.
Cheers, G.
In some cases, I’ve opted for the simpler asterisk method. Meaning, my scenario might read:
* Logging in with valid credentials will take the user to the dashboard.
This is a pretty concise way to explain to a user exactly what they should expect. And BDD often gets constrained by the notion that everything should be Given-When-Then, when in fact, what we want is simply the best possible expression to make how value is experienced clear.
So with the above, I might have helper methods that handle “logging in with valid credentials.” (They could also handle “logging in with INVALID credentials” and so on.) So the whole step is basically executed as code that would (1) grab the data for valid credentials (using a data builder pattern), (2) instantiate the login page, (3) apply the credentials to the login fields, and (4) make sure the dashboard is what appeared.
Note, however, that even in your case, the Then step might be checking for some specific aspect of the dashboard. So, yes, your When step did in fact lead to the dashboard being instantiated and thus the constructor checks the title, perhaps. Those are more assertions. But you can also then have expectations: meaning, your Then step checks if certain elements are present on the dashboard page. (Perhaps a text that says “Logged in as Whomever” or whatever else you expect the user to see.)