📜 ⬆️ ⬇️

Page Object Model + Webdriver. An example of implementation on a single test

I decided to write this article, because I think this approach is the most effective for organizing the structure of a test automation project.
Unfortunately I did not work with other automation tools other than Webdriver or Selenium. But despite this, it seems to me that this approach can be used with other tools.

Code samples will be on C # + NUnit.



Just want to say that this article is a reflection of my own experience and does not contain any references or quotes. What is written below is nothing more than a narration of my vision of this approach and is not at all presented as a postulate.
')

Why is it so effective?



Probably because he brings this invaluable order to the structure of the project. Following the principles of this approach, we create a structure with well-defined logical modules. And each such module will be a reflection of the logical modules of the application under test.
This greatly facilitates the search and reuse of methods and, as a result, facilitates project maintenance. Also, it significantly reduces the time required for a new participant to “inject” into the project.

Structure



An example would be one simple test - a login on Facebook (success in logging in, we assume that the username appears in the upper right corner of the page).

An example of this project can be downloaded here github.com/DmitryRoss/Page-Object-Model-Article .

- At the head of the entire project are classes with NUnit tests. For our test, we will make one such class and call it LoginTests. In it we will create a test method with the name AssertLogin ().

[TestFixture] public class LoginTests : BaseTests { static LoginHelper loginHelper = new LoginHelper(); [Test] public static void AssertLogin(){...} } 


- Below are the Helpers classes. Each such class serves one specific class with tests.
But sometimes, some methods from the Helpers classes are needed by other classes with tests or other Helpers classes. In this case, it is advisable to bring such methods into separate general classes that are at the same level in the hierarchy of the structure as the Helpers classes. A striking example of this class is the application navigation class. After all, the transition to this or that page may be needed in different classes with tests or classes Helpers. Usually in each project there are other Helpers classes that are rendered as general.
For convenience, the methods return an instance of the Helper class in which they are located. This allows you to contact them in such an abbreviated form, through the point -

  loginHelper. DoLogin(userName, password). AssertUserName(displayedUserName); 


In our example, we will create one such class and name it LoginHelper.
In it we will create 2 methods.
1. DoLogin (string userName, string password)
2. AssertUserName (string userName)

 public class LoginHelper :TestFramework { public LoginHelper DoLogin(string userName, string password){...} public LoginHelper AssertUserName(string userName){...} } 


- And here begins the core of our project - Pages, i.e. pages. These are classes with pages of our application. Those. Each such class corresponds to a specific page of the application and it contains the locators and web elements of this page. In addition, it has elementary methods that are used on this page by Helpers classes.
I believe that it is good practice to divide it into as small as possible elements. For example, if a pop-up window appears on your page, it is better to create a separate class for this window. Any fragmentation should be obvious and reflect something in the application itself.
The approach also described in this article assumes that each method should, if possible, return a copy of the page to which it will lead. For example, clicking on the Login button takes us to the main page. Consequently, the method of clicking on the Login button returns an instance of the class in which the main page is described. If the actions in the method do not involve going somewhere, then it returns the instance of the page class in which it is located.
This allows you to conveniently refer to the page methods -

 loginPage. TypeUserName(userName). TypePassword(password). ClickLoginButton(); 


Obviously, the TypeUserName method does not lead to a transition to any other page, so this method returns us an instance of the LoginPage page (in which it is located). The same applies to the method - TypePassword. But the ClickLoginButton method goes to the main page, so it will return an instance of the LandingPage class. If we had to call any method from the main page (LandingPage), then we would just put a period (.) And immediately get access to all methods of the main page.
Let's go back to the example. We have two pages - the page from which we Login to the system and the page to which we get after Login.
Create the LoginPage and LandingPage classes for them.
On the page from which we log in, we perform 3 logically understandable actions (as part of our test) - fill in the field with the user name, fill in the field with a password, and click on the enter button.
On the page where we go, we perform only one action (as part of our test) - we make sure that the username is displayed correctly in the upper right corner of the page.

 public class LoginPage : TestFramework { [FindsBy(How = How.XPath, Using = USER_NAME_TEXT_FIELD)] public IWebElement userNameTextField; [FindsBy(How = How.XPath, Using = PASSWORD_TEXT_FIELD)] public IWebElement passwordTextField; [FindsBy(How = How.XPath, Using = LOGIN_BUTTON)] public IWebElement loginButton; public static LoginPage GetLoginPage(){...} public LoginPage TypeUserName(string userName){...} public LoginPage TypePassword(string password){...} public LandingPage ClickLoginButton(){...} public const string USER_NAME_TEXT_FIELD = "//input[@id='email']"; public const string PASSWORD_TEXT_FIELD = "//input[@id='pass']"; public const string LOGIN_BUTTON = "//label[@id='loginbutton']/input"; } 


In the example above, there is a GetLoginPage () method. It returns an instance of the class of this page with initialized elements.

I would also like to highlight some convenience of using inheritance from pages. For example, in most web applications, the upper part (Header) is displayed on all pages. In this case, it is very convenient to describe Header in a separate class, and make all other pages the heirs of this class. Then the Header methods will be available from any page.
You can also use inheritance when clicking on a checkbox leads to the appearance of the whole form. Based on the principle of maximal dorobeniya, it is better to take out this form into a separate class. But at the same time all the elements of the page itself remain visible. Here it is advisable to make the form a page heir so that the page elements and methods are accessible to it.

I often saw projects being built without a layer with Helpers classes. Tests directly addressed the methods of the pages.
This approach has the following disadvantage. In any automation project, methods that combine the elementary actions of several pages are needed. In our example, there is a method, DoLogin (). I did not fully describe it. If I wrote it correctly, then at the beginning it would be necessary to make sure that no one is logged in, exit, if logged in and go to the page with which we enter. Further our method would go and, after its termination, it would be necessary to be convinced that the main page was loaded. I did not implement this for ease of transferring the basics of the described approach.
But, if we put this method in the LoginPage page class (as in projects without a layer with Helpers classes), then we would lose this clear boundary between logical modules. After all, the procedure for entering the system does not begin on the login page.
But imagine the situation when you need more methods to check the validation of fields to enter the system. In this case, the LoginPage class will be overgrown with a bunch of methods that only the LoginTests class will need.
Such an excessive amount of complex methods in the class page will, on the one hand, make it difficult to search for elementary methods, and on the other hand, make a blurred line between logical modules.

But creating Helpers classes has one pitfall. What to do with the methods that are used in class 1 test is clear - they will be in the Helper classes of this test. But what to do with other complex methods that can be used by other test classes?
Here I proceed as follows: I single out separate classes with complex methods (that is, methods that use several elementary methods from one or several page classes.), Let's call them Utils classes. Names will be of type NavigationUtils. From the name of the class it is clear that it contains methods for navigating in the application. Or LoginUtils - login methods that can be used by other classes, in addition to the class with login tests. If you need to create some content in the application to perform different tests, then you can create the ContentUtils class.
My rule is simple - if this method from one Helper class was needed by some other class with tests or Helper classes, then this method migrates to the corresponding Utils class. If this Utils class has not yet been created, then I create it.
Such an approach will somewhat complicate the project with an additional layer, but it will put a clear line between logical modules. Because in the classes of the pages only the most elementary methods will be located, which can be performed only on this page.

A little more about the service.



In projects where I participated, the lion's share of changes in the application from build to build accounted for locators. Another small share fell on the ways of influencing the elements (for example, what was previously worked out by the usual Click () command, after the release of the new build, it already required a series of commands from MouseMove (), MouseDown and MouseUp ()). In such cases, all support comes down to changing the classes of the pages. And, as you can see, these are the most reusable classes. Consequently, support efforts are minimal.
There are cases when in a new build some logical chain of actions changes, for example, a link to navigate to an entity in an application is transferred from one menu to another. Support will then affect Helpers classes and Utils classes.
And extremely rarely, when the logic of the test itself changes, you will need to support classes with tests.

Perfect structure.



For myself, I derived the ideal structure of such a project, but, I confess, it didn’t follow, it only worked towards it.

This is a set of simple rules:
1. Classes with tests use only Helpers classes and Utils classes. Do not use page classes, and, especially, the framework methods that work with Selenium and Webdriver. The remaining methods of the framework can be used.
2. Helpers classes use only page classes and Utils classes. Do not use framework methods that work with Selenium and Webdriver. The remaining methods of the framework can be used.
3. Utils classes use only page classes. Do not use Helpers classes and framework methods that work with Selenium and Webdriver. The remaining methods of the framework can be used.
4. Page Classes use only framework methods. Do not use the methods of Selenium and Webdriver directly.
Observing such rules initially, I think, will help avoid painful problems with the project in the future and will make support as easy as possible.

From my point of view, many existing projects could be converted in accordance with the described approach. I myself had such an experience. And I tell you, such a transition revealed a huge amount of duplicate code.

Those who have been in large testing automation projects understand that without order there simply cannot survive. Therefore, if you still have not heard / thought / tried it, I still recommend you to try.

Source: https://habr.com/ru/post/155109/


All Articles