Web Automation: Selenium WebDriver and Python — Getting Started — Part 3
Before we dive in…
In the previous episodes (Part 1, Part 2) of this beginner tutorial, we setup environment for writing automation scripts in Python using Selenium on Windows OS. We also used python’s builtin testing library, ‘unittest’ to organize our tests and used ‘Html-testRunner’ package to generate HTML reports.
In this part, we take our first dig as ‘Page Object Model’ design pattern to create scripts which are less flaky, have organized code, incorporate code reuse and give tester the flexibility to easily understand and write scripts for new usecases/ testcases.
What is Page Object Model (POM)
According to the official documentation for selenium in python,
“A page object represents an area in the web application user interface that your test is interacting.”
And also for the benefits of POM, documentation states:
Creating reusable code that can be shared across multiple test cases
Reducing the amount of duplicated code
If the user interface changes, the fix needs changes in only one place
The benefits above are precisely what we endeavour to do in this part of tutorial.
Our Approach
We have so far managed to create 5 test methods which test 5 testcases. We put them in one class and also created setUp and tearDown methods, which execute before and after every test method, respectively. If we examine our test methods closely, we would see that:
- AMZN_Search_TC_001 used Amazon’s Home Page
- AMZN_Search_TC_002 used Amazon’s Home Page and Search Results Page
- AMZN_Search_TC_003 used Amazon’s Home Page, Search Results Page, Product Details Page and Sub Cart Page.
- AMZN_Search_TC_004 used Amazon’s Home Page, Search Results Page, Product Details Page, Sub Cart Page and Cart Page.
- AMZN_Search_TC_005 used Amazon’s Home Page, Search Results Page, Product Details Page, Sub Cart Page and SignIn Page.
Therefore, we start with making class for each of these pages. We will create a Base Page class also which all of these page classes would inherit.
After doing that, next task would be to put the locators and methods related to different pages in their respective classes. Here, there are two approaches to maintain the locators. First is keeping all the locators specific to a page in its own class and the other is to keep locators of all pages at a common place. We will take the latter one. Methods specific to a page would obviously be kept in that page’s class.
Also, we will keep all the test data like URL of the application, search term etc. , in a separate file. This will help us change the TestData and test again to make sure our scripts work fine with different sets of test data.
After the page classes, locators and test data are in place, the next task would be of writing the testcases.
The folder structure for our POM project would be like the one below:
Amazon (project’s name)|- Drivers|- Reports|- Resources |- PO|- Tests
where, ‘Drivers’ folder stores all the executables for different browsers that we want to be used (like ChromeDriver.exe etc.). ‘Reports’ folder would have the test execution reports. ‘Resources’ folder stores the locators, test data, helper methods (if any) and ‘PO’ folder, which contains page object classes. ‘Tests’ would contain the actual tests that we want to execute plus the testsuite file.
NOTE: In a page class, in practice, complete page is modeled. That is, all of the elements on that page are identified and stored using their respective locators and the functions/ features page performs/ has are modeled using methods.
In our this tutorial, we will model only those elements and features on different pages of Amazon’s website which we are using in our testcases.
TestData
The website’s URL, search term etc. is the test data for our testcases and we should keep them in dedicated python file. Therefore, let us identify all the strings, numbers etc. that we have used in our tests and put them in TestData.py
.
TestData.py
We have kept the the test data in a class but readers are free to explore other options also.
Locating the Locators
Ok, so much for a heading 😁, especially, when we already have identified all the locators we need for our testcases. However, we still need to put them at one place. We do this so that we have a single point of reference when it comes to updating any locator(s), in future. Let’s name the file as ‘Locators.py
’ and place it in the ‘Resources’ folder. We will take all the locators we have used in out testcases and place them in this file with proper names and the locator strategy (being located by ID
or CLASS
or XPATH
etc.) that they are being used with. So, after all of our locators have been put in a new python file, they should look like:
Locators.py
Observe that each Locator is actually a python tuple. A Tuple is a collection of Python objects separated by commas.
Before we move towards creating the BasePage
class and then inherit pages from it, a brief discussion on how do we want our browser to wait for elements, is necessary.
Waiting for element OR Waiting for event
Yes, the heading is a bit tricky to understand in the first read, especially when we as automation testers are habitual of reading/ using implicit wait
and explicit wait
.
We have used built in function implicitly_wait()
in our tests in the previous tutorials. We will take completely opposite approach for our ‘framework’, however.
‘Implicit Wait’ means the time our script would wait while searching for any web element on the page, thus, ‘waiting by element’. We set it using implicitly_wait(<time in seconds>)
function (in Python) and once it is set, it is valid for the time till script is executing or in other words for the lifetime of the WebDriver
object. The time that we specify is dependent on how much time does most of the web elements take to load on different pages of our application.
While it is easy to use (specify once and need not bother later throughout the script), it can be abused also if not carefully thought after. If we specify a large value for ‘time’ in the implicitly_wait()
, it can increase the execution time of our scripts.
‘Explicit Wait’ means the time our scripts waits for an event/ condition to occur. This implies that now our script will not wait for all elements on the web page to load but for some specific elements to load. It is very useful especially when dealing with dynamic pages. To explicitly wait for an element, in Python, we use WebDriverWait
and Expected_Conditions
. For example,
from selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditionsdriver=webdriver.Chrome()WebDriverWait(driver,10).until(expected_conditions.visibility_of_element_located(By.ID(“twotabsearchtextbox”)))
In the last statement above, we are using WebDriverWait
object and we are passing our webdriver object and time (in seconds) to wait, as arguments. This object will until
some expected conditions occurs, which in our example is, visibility_of_element_located
. The element under consideration is being identified its ID
locator and the value of the locator. This statement, however, is incomplete, since neither are we storing the web element returned by this statement in any variable, nor are we performing any action like click()
or send_keys()
etc on it. We will soon see how we can we make use of it.
expected_conditions
which are frequently used are as below:
title_is , title_contains , presence_of_element_located, visibility_of_element_located, visibility_of, presence_of_all_elements_located, text_to_be_present_in_element, text_to_be_present_in_element_value, frame_to_be_available_and_switch_to_it, invisibility_of_element_located, element_to_be_clickable, staleness_of, element_to_be_selected, element_located_to_be_selected, element_selection_state_to_be, element_located_selection_state_to_be, alert_is_present
For interested readers, the official documentation link for ‘waits’ is here.
We will use ‘explicit wait’ in our script from now on.
Base Page Class
Base Page Class is the one from which classes of all other pages inherit. We will therefore create it first, populate it with methods for the most common actions that are expected to be performed on any page in the application. The base page class should look something like this:
BasePageClass
NOTE: The __init__()
function is called automatically every time the class is being used to create a new object.
As the code is well commented, we need not put more explanation to it. Still, we would like to reiterate that what is being presented above is the final product which has evolved as we progressed through test script creation. We started with the click()
and enter_text()
methods and kept adding more methods as per the requirements. Keeping the common methods in the base page class has obvious advantages. Firstly, all page classes that inherit the base page class, have these methods, to use. Secondly, if we have to update these methods or add more common page methods, we need to do so at only one place (i.e. BasePage
class).
Other Page Classes
Now that the base class is with us, let’s put together the classes for other pages also. For the sake of simplicity we have kept all of the page classes (including the base page class) in one file Pages.py
. Readers are free to organize their classes as they like.
Let’s first create the HomePage
class. We will be deriving it from BasePage
class and in its __init__()
method we will make a call to the __init__()
method of its parent/ super class, i.e., the BasePage class. We will also implement search() method which reads the search term from the TestData class. After we have done that, the HomePage class should look like this:
Logically next page to model would be the SearchResultsPage
and so would we do. Since this class will also be derived from the BasePage
class, its __init__()
method will also call its super class’ __init__()
method. On this page, the only operation that we performed was clicking on the first search result, so we will write a click_link()
method to do exactly that.
We should note here that within the click_search_result()
, we have called the click()
method of parent class.
Next, we will model ProductDetailsPage
, SubCartPage
, CartPage
, SignInPage
classes in the similar manner. For each of the task that we performed on these pages, in our testscripts in the previous part of our tutorial, we create class methods, like add-to_cart_button()
, click_cart_link()
, and delete_item()
.
The complete page.py
should then look like this:
Pages.py
An important python basic that is being used in the search()
method of HomePage
class is unpacking of the tuple Locator.SEARCH_TEXTBOX
while we are clearing it out before we enter the search term. Interested reader may want to read this to learn more.
We can see that throughout the PageClasses, we have simply transferred statements from our previous testscripts and instead of using the locators directly, we are now referencing them from one file. Now, when we write our tests all that we need to know is the testdata and the method of the page class to be used. Let’s get on to tests now.
Tests
Just like base page class, we will have Test base class. This class shall have the setUp()
and tearDown()
methods. As we did in the previous part of this tutorial, we will create the `driver` identifier in the setUp()
method, using chrome executable’s path, to load the browser and we will close the browser in tearDown()
method.
To test the journey from loading the home page to searching an item to adding an item to cart to checking out, we will create one `Test_AMZN_Search
` class and write all of our tests in the same class. Test methods do not have any value if they do not assert the functionality being tested and since we are using python’s builtin ‘unittest’ module for the purpose of running our tests and consequently generate report, we will use its ‘assert’ methods.
Below is the TestBase
class and our first test case method which tests if the home page of Amazon has loaded successfully or not.
Notice that we have named our test method to be pretty descriptive so that any one who reads it, is able to make out the purpose of the test method. Let’s first execute this method and see how has our framework worked out so far.
And… we got an exception 😢. Python is not able to locate the file ‘Locators.py’ in ‘Resources’ folder despite the fact that we did import it from the ‘Resources’ folder 😕.
Upon googling we learnt that if we want to read/access/import any file/code from other folder, in Python, then we need to provide access to that file, in the file where we want to access it. To do so, we will need to import a couple of builtin python modules, namely, ‘os’, ‘sys’ and, ‘inspect’ and we will also need to create ‘__init__.py’ files in all folders from where we want to access/ import python file/code. You may want to read here, for more information. So now our AllTests.py looks like:
Before we move any further, let’s recall that in our Pages.py
file also we have used ‘Locators’ and ‘TestData’, therefore, we will need to do the same exercise in that file also. Let’s do that first. So, now our Pages.py
file looks like this:
We will run the test again to check if what ever changes we have made have been successful or not.
Yay! 😄, the test ran successfully this time. Observe that we have executed the testcase by name. This is pretty useful when there multiple tests and you want to execute a specific test (we will be discussing this in detail in a future tutorial). Also, observe the report was also generated with date and time stamp. Let’s see the report
Ok, all good here. Now, let’s quickly jump to the next test where we test for entering a search term on home page and testing that the search results page is loaded successfully. The test should look like this:
Again, we have kept the name of the test descriptive enough for the reader to understand its purpose. Since we know the intent of the test and the statements in the test have comments preceding them, we will shy away from duplicating the explanation, here, and progress towards writing remaining three cases.
Complete test class with all tests should look like this:
We will now execute all of the tests to see the end result.
Phew… 😌, everything passed. Observe the ‘Ok’ towards the end of execution for each testcase and at the end of execution of all tests. It’s just ‘Ok’ for Python, well….
The output of this execution has also been generated as an HTML report, thanks to HTMLTestRunner
module and it looks like:
This is pretty, yet not that informative as the console output. For example, the report shows the total duration of execution but does not display the time taken for each test to execute (which is shown in the console). We will work on this in a later part of this tutorial.
Observant readers must have also noticed the order in which testcases got executed. We can manage the order by naming the testcases as per our desired order, as we did earlier. So, for example, test_home_page_loaded_successfully
may be renamed to test_01_home_page_loaded_successfully
or test_TC_001_home_page_loaded_successfully
.
Food for Thought
In this part of our tutorial, we have managed to put up a barebones automation framework for our application under test. A lot of work is still needed to be done. There is no exception handling in our framework. Also, we need to provide for taking screenshot(s) if test(s) fails. We need to work upon creating and running testsuites, running tests in parallel, better reporting, logging. We have not touched upon the CI/ CD part of it, so we need to work upon that as well. In the following parts of this tutorial we will be working upon these and a couple of things more.
If you have any suggestions on this tutorial or for me in general, please do leave in comments. I will surely try to improve.
Until then, keep learning, keep testing. Best wishes!