This article is about challenges with an automation of web application tests and how to solve them using the most popular design pattern in test automation. The solution applies whenever you have an existing project or you start from scratch.
NOTE: All examples are written in C# and dotnet core. you can find complete solution in github.
The problem
One of the main challenges with automated testing is to create test suite which is easy to maintain and gives us quick answer about quality of SUT (system under test). If the automation suite is not implemented well we will end up with fixing and maintaining the whole suite over and over again forgetting about its main purpose to help us in every day job.
So what is the Page Object Pattern and how does it help our automation to succeed? Imagine your automation is implemented in the way we get elements and interact with them within every test scenario. Of course its the easiest way to start with automation but quickly enough we realize we duplicate many lines of code and it becomes more and more unreadable. So let's see following example with simple test of login:
[Test]
public void LoginTest()
{
IWebDriver driver = new ChromeDriver();
driver.Navigate().GoToUrl("https://qa-services.github.io/simple-test-page/login.html");
IWebElement txtEmail = driver.FindElement(By.CssSelector("#email"));
txtEmail.SendKeys("test@test.com");
IWebElement txtPassword = driver.FindElement(By.CssSelector("#password"));
txtPassword.SendKeys("Test1!");
IWebElement btnLogin = driver.FindElement(By.CssSelector("#sign-in"));
btnLogin.Click();
IWebElement btnLogout = driver.FindElement(By.CssSelector("#logout"));
Assert.IsTrue(btnLogout.Displayed, "Check if logout button is displayed");
driver.Quit();
}
Now imagine you have test suite of 50 or 100 tests and whole automation is written as above example. Some day there is a UI change of the login page and all selectors are different for example. We have to change all of our test scripts and fix all the selectors. If whole automation would be written that way we would end up with inflexible project which requires lots of maintenance.
To sum up the main problem is we end up with:
- Test scenarios which are difficult to read;
- Lots of duplicated code;
- UI changes breaks multiple tests in multiple places.
Page Object Pattern to the rescue
Page Object Pattern (POP) is a set of rules and good practices to solve previously mentioned challenges. So instead of interacting with elements directly from the tests we create PageObject class which represents the UI page we are going to interact with. Simply speaking we create a class which contains WebElements
of particular page and set of methods which represents services that the page offers. Additionally methods of PageObjects should return other PageObjects. Thank to that we model the user experience through our application. Let's have a look at the example.
Page Objects
Consider we have a login page and after successful login we are redirected to home page. (Credentials - user: test@test.com, password: Test1!)
According to POP we create PageObjects for those pages as follows:
// LoginPage.cs
public class LoginPage
{
private IWebDriver driver;
public LoginPage(IWebDriver driver)
{
this.driver = driver;
}
private IWebElement TxtEmail => driver.FindElement(By.CssSelector("#email"));
private IWebElement TxtPassword => driver.FindElement(By.CssSelector("#password"));
private IWebElement BtnLogin => driver.FindElement(By.CssSelector("#sign-in"));
private IWebElement ElmError => driver.FindElement(By.CssSelector(".errors-container>h4"));
public LoginPage TypeEmail(string email)
{
TxtEmail.Clear();
TxtEmail.SendKeys(email);
return this;
}
public LoginPage TypePassword(string password)
{
TxtPassword.Clear();
TxtPassword.SendKeys(password);
return this;
}
public LoginPage SubmitWithFailure()
{
BtnLogin.Click();
return this;
}
public HomePage Submit()
{
BtnLogin.Click();
return new HomePage(driver);
}
public HomePage Login(string email, string password)
{
TypeEmail(email);
TypePassword(password);
BtnLogin.Click();
return new HomePage(driver);
}
public LoginPage AssertLoginErrorIsDisplayed(string expError)
{
Assert.IsTrue(ElmError.Displayed, "Check whether error is shown.");
Assert.AreEqual(expError, ElmError.Text, "Check error text.");
return this;
}
}
// HomePage.cs
public class HomePage
{
private IWebDriver driver;
public HomePage(IWebDriver driver)
{
this.driver = driver;
}
private IWebElement BtnLogout => driver.FindElement(By.CssSelector("#logout"));
public HomePage AssertLogoutButtonIsDisplayed()
{
Assert.IsTrue(BtnLogout.Displayed, "Check whether logout button is shown.");
return this;
}
}
In LoginPage
class you can see WebElements
defined just below class constructor and after that methods representing users actions in the page. Please notice there are two similar methods Submit
and SubmitWithFailure
. Body of those methods are the same but the returning type is different. This is because when we have a negative scenario e.g. login with incorrect email, wrong password etc. we do not navigate to the HomePage
but stay at LoginPage
so there we will use SubmitWithFailure
method. Of course some advanced programmer can implement generic method and provide returning type during invocation but let's keep it simple here.
Writing tests
Let's implement successful login test using PageObjects above.
[Test]
public void SuccessfulLoginTest()
{
var loginPage = new LoginPage(driver);
loginPage.TypeEmail("test@test.com");
loginPage.TypePassword("test");
var homePage = loginPage.Submit();
homePage.AssertLogoutButtonIsDisplayed();
}
Pretty easy and readable, isn't it?
You may already have noticed that there is also Login
method within LoginPage
class. The reason I have put this method there is simple - in long end to end test scenarios we would need to use elementary methods TypeEmail
, TypePassword
, Submit
every time we need to login so our tests would become long and again unreadable. Instead of those three methods we can just use Login
. Lets see it in action:
[Test]
public void SuccessfulLoginTestUsingLoginMethod()
{
var loginPage = new LoginPage(driver);
var homePage = loginPage.Login("test@test.com", "Test1!");
homePage.AssertLogoutButtonIsDisplayed();
}
Methods Chaining
I have also made one thing against the law. One of the principles of Page Object Pattern is to avoid assertions within the PageObject classes. The reason I've done it is I like to be "fluent". As you have noticed all PageObject methods return other PageObject type (when I end up on Home Page after some method I should return type of the HomePage
). This gives me the possibility to use method chaining aka fluent interface. But let's see an example:
[Test]
public void SuccessfulLoginTestWithChaining()
{
new LoginPage(driver)
.TypeEmail("test@test.com")
.TypePassword("Test1!")
.Submit()
.AssertLogoutButtonIsDisplayed();
}
[Test]
public void NegativeLoginTestWithChaining()
{
new LoginPage(driver)
.TypeEmail("test@test.com")
.SubmitWithFailure()
.AssertLoginErrorIsDisplayed("Invalid login attempt.");
}
NOTE: If you feel uncomfortable to implement assertion methods within the PageObjects you can use partial class in C# and create partial class of LoginPage
called e.g. LoginPageAsserts.cs
and keep all Assert methods in this class.
Summary
Using POP the UI changes does not affect our test logic - we change only required PageObject in one place and to not touch tests. It makes tests readable and whole project is easy to maintain.
Base rules when you implement PageObject classes:
- PageObject should contain elements of the page
- PageObject public methods should represent user actions on the page
- PageObject methods should return other PageObjects
- Do not implement everything what the page offers, only necessary things to complete actual tests.
Hope you enjoy this article. Feel free to like and share.
Please find github demo project with above examples here.