Web Automation with Selenium & Python Part 3: [Login Automation]

In our last episode, we automated the homepage. Now its time to do a login automation test. Well, when a QA test that manually, what he/she does is typing email/username and password. And submit the login/signup button of the login page. And while they are testing, they try with both wrong and correct credentials. But this is what they had to do more often. Well if you need to check a feature that requires authentication, you need to log in. But we are not gonna do that again by our hand from today. I will demonstrate the login automation now. But first, let’s see our updated application structure.

The Automation Testing Application Structure

.
├── application
│   ├── app.py
│   ├── __init__.py
│   └── utils
│       ├── constants.py
│       ├── helpers.py
│       ├── __init__.py
│       └── urls.py
├── homepage
│   ├── homepage.py
│   ├── __init__.py
├── login
│   ├── __init__.py
│   ├── login.py
├── README.md
├── requirements.txt
└── settings.py
└── .env

Last time we worked on the homepage package. Today we have a new package called login. And we have a file called login.py under it.

Let’s Start The Login Test Automation

In most cases, there will be a user/email field, a password field, and a submit button. And in this automation testing, we have to work on those fields. So let’s meet our requirements.

  1. Login with correct credentials.
  2. Failed login with wrong credentials.
  3. After a successful login, we will visit the login page again. And check if it’s redirecting us to the dashboard or not.
  4. Log out.

[ Step:00 ] Use Pylint and Kite For better Performance

You can use Pylint in 2 ways.

  1. Install it in your system
  2. Integrate with your IDE

If you are a Linux user then its really easy.

sudo apt-get install pylint

and then use it for any python file like this:

pylint example.py

And it will show you where you can improve in your python code.

Now about the kite. Kite is the AI assistant giving developers superpowers. It uses machine learning to show you auto compilation and has a co-pilot mode to help you know the code better with documentation. If you are a Linux user then run this command below.

bash -c "$(wget -q -O - https://linux.kite.com/dls/linux/current)"

[ Step:01 ] Environment Variables

I will be working with the admin login part. And I will leave the user login to you guys. Here are the credentials in our .env file.

ADMIN_EMAIL=admin@email.com
ADMIN_PASSWORD=123456

USER_EMAIL=user@email.com
USER_PASSWORD=123456

WRONG_EMAIL = 'xyz@xyz.com'
WRONG_PASSWORD = 'xyz'

To access .env we need to install python-dotenv package.

pip install python-dotenv

And now we will access them from settings.py file.

"""Application Settings"""

import os
from dotenv import load_dotenv

load_dotenv()

ADMIN_EMAIL = os.getenv("ADMIN_EMAIL")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")
USER_EMAIL = os.getenv("USER_EMAIL")
USER_PASSWORD = os.getenv("USER_PASSWORD")

WRONG_EMAIL = os.getenv("WRONG_EMAIL")
WRONG_PASSWORD = os.getenv("WRONG_PASSWORD")

[ Step:02 ] Add Constants And URLs In Our Application

In our application/utils/urls.py file lets add these URLs.

ADMIN_DASHBOARD = "https://querkiz.itech-theme.com/admin"
USER_DASHBOARD = "https://querkiz.itech-theme.com/home"

And in our application/utils/constants.py let’s add those constants of required XPath.

LOGIN_EMAIL_FIELD_X_PATH = '/html/body/div/div/div/div/div/div/div/form/div[' 
                           '1]/input '

LOGIN_PASSWORD_FIELD_X_PATH = '/html/body/div/div/div/div/div/div/div/form' 
                              '/div[2]/input '
LOGIN_SUBMIT_BUTTON_X_PATH = '/html/body/div/div/div/div/div/div/div/form' 
                             '/button '
DASHBOARD_UPPER_RIGHT_MENU_X_PATH = '/html/body/div[1]/div[2]/div[' 
                                    '1]/div/div/div[2]/div/button '
LOGOUT_LINK_X_PATH = '/html/body/div[1]/div[2]/div[1]/div/div/div[' 
                     '2]/div/div/a[3] '
LOGIN_PAGE_X_PATHS = dict(email_field=LOGIN_EMAIL_FIELD_X_PATH,
                          password_field=LOGIN_PASSWORD_FIELD_X_PATH,
                          submit_button=LOGIN_SUBMIT_BUTTON_X_PATH)

[ Step:03 ] Add Helper Methods

In our last episode do you guys remember that we used some code to print colored messages right? Did you notice that I broke the DRY principle there? If you do so then I thank you. Let’s refactor our code to follow the DRY principle. We will create a method called console_print() which will do the job.

"""Helper methods"""
from termcolor import colored


def console_print(message_type: str, message: str):
    """
    Print console message with color both for success and fail
    :param message_type: success or failed
    :param message: message to print
    """
    status, color = ('[success] ', 'green') 
        if message_type == 'success' 
        else ('[failed] ', 'red')
    state = colored(status, color, attrs=['bold'])
    message = colored(message, 'white')
    console_message = state + message
    print(console_message)

This method will take two parameters called message_type and message. message color will depend on the value of message_type. If it is “success” then the color will be green and if not then it will be red.

[ Step: 04 ] Developing Login Package

Write those below codes in our login/login.py file

"""
    LoginTest module will test:
    1. Successful login
    2. Failed login with wrong credential
    3. Secondary Login attempt in a new tab after a successful login.

    If users redirect to the dashboard url or any given url, then login attempt
    will be successful

"""

from time import sleep
from selenium import webdriver
from selenium.common.exceptions import ElementNotInteractableException, 
    NoSuchElementException, JavascriptException, TimeoutException
from application.utils.helpers import console_print


class LoginTest:
    """
        LoginTest class test successful and failed login attempt as well as try
        to login after a successful login in a new tab. Log out testing can
        also be done.
    """

    def __init__(self, url: str, login_page_x_paths: dict, dashboard_url: str):
        """
        :param url: Web application's login url
        :param login_page_x_paths: form and button XPaths
        :param dashboard_url: dashboard url after login redirection
        """

        self.url = url
        self.login_page_x_paths = login_page_x_paths
        self.dashboard_url = dashboard_url
        self._browser = webdriver.Chrome()

    def visit_login_page(self):
        """
        Visit the web application's given login url
        :return: bool
        """
        try:
            self._browser.get(self.url)
            console_print('success', '[Login page OK!]')
            sleep(2)
        except TimeoutException as error:
            console_print('failed', '[Login page not OK!]')
            console_print('failed', str(error))
            self._browser.quit()

            raise

    def login_attempt(self, email: str, password: str):
        """
        Attempt a login with given correct credentials
        :param email: given user email
        :param password: given user password
        """
        self.login(email, password)
        redirected_to_dashboard: bool = self.current_url_is_dashboard_url()
        console_print('success', '[Login is working!]') if 
            redirected_to_dashboard else 
            console_print('failed', '[Wrong credential, Login failed!]')

    def secondary_login_attempt_in_new_tab(self):
        """
        After make a successful login attempt we will try to open a new tab and
        visit the login page again, generally it will redirect us to the after
        login redirect url which is dashboard in most cases.
        """
        self.open_and_switch_to_new_tab()
        self.visit_login_page()
        self.current_url_is_dashboard_url()
        console_print('success', '[Secondary login attempt redirected to '
                                 'dashboard!]')

    def open_and_switch_to_new_tab(self):
        """
        Execute javascript to open new tab and then switch to the new tab
        """
        try:
            self._browser.execute_script("window.open('');")
            sleep(1)
            self._browser.switch_to.window(self._browser.window_handles[1])
            console_print('success', '[New tab opened!]')
        except JavascriptException as error:
            console_print('failed', '[New tab open failed!]')
            console_print('failed', str(error))
            self._browser.quit()

            raise

    def current_url_is_dashboard_url(self) -> bool:
        """
        Check the current url and if it is the dashboard url then return true
        otherwise return false
        :return: bool
        """
        current_url = self._browser.current_url
        if current_url == self.dashboard_url:
            console_print('success', '[Current url is dashboard url!]')

            return True
        console_print('failed', '[Current url is not dashboard url!]')

        return False

    def login(self, email: str, password: str):
        """
        Invoke methods to set email and password and submit the login form
        :param email: user email
        :param password: user password
        """
        self.set_email(email)
        self.set_password(password)
        self.submit()
        sleep(2)

    def set_email(self, email: str):
        """
        Set email to the email field which will be selected by its XPath
        :param email: user email
        """
        try:
            email_field = self._browser.find_element_by_xpath(
                self.login_page_x_paths['email_field'])
            email_field.send_keys(email)
        except (
                ElementNotInteractableException,
                NoSuchElementException) as error:
            console_print('failed', '[Email input failed!]')
            console_print('failed', str(error))
            self._browser.quit()

            raise

    def set_password(self, password: str):
        """
        Set password to the password field which will be selected by its XPath
        :param password: user password
        """
        try:
            password_field = self._browser.find_element_by_xpath(
                self.login_page_x_paths['password_field'])
            password_field.send_keys(password)
        except (
                ElementNotInteractableException,
                NoSuchElementException) as error:
            console_print('failed', '[Password input failed!]')
            console_print('failed', str(error))
            self._browser.quit()

            raise

    def submit(self):
        """
        Get the submit button by its given XPath and click the button to submit
        the login form
        """
        try:
            submit_button = self._browser.find_element_by_xpath(
                self.login_page_x_paths['submit_button'])
            submit_button.click()
        except (
                ElementNotInteractableException,
                NoSuchElementException) as error:
            console_print('failed', '[Credential submit failed!]')
            console_print('failed', str(error))
            self._browser.quit()
            print()

            raise

    def logout(self, menu: str, logout_link: str):
        """
        Logout from dashboard
        :param menu: Upper right menu in dashboard
        :param logout_link: logout link
        """
        try:
            self._browser.find_element_by_xpath(menu).click()
            self._browser.find_element_by_xpath(logout_link).click()
            console_print('success', '[Logout successful!]') if 
                self._browser.current_url == self.url else 
                console_print('failed', '[Logout failed!]')
        except (
                ElementNotInteractableException,
                NoSuchElementException) as error:
            console_print('failed', '[Logout failed!]')
            console_print('failed', str(error))
            self._browser.quit()
            print()

            raise

[ Step: 05 ] Code Breakdown & Explanation

Hold your horse. I’m not gonna let you just copy and paste the code. I need you to understand them.

Initialize the attributes of LoginTest Class

In our __init__  method we are initializing url, login_page_x_paths, dashboard_url. Also, we have initialized our web driver as _browser. Let’s break more,

self._browser = webdriver.Chrome()

This is how we will initiate our web driver. And we used the Chrome web driver.

Visit login page

Our visit_login_page() method is responsible for visiting the given URL. We will hit the login page URL.

self._browser.get(self.url)

After that, we will print a colored message. And wait for 2 seconds.

console_print('success', '[Login page OK!]')
sleep(2)

But there could be an exception like a timeout. Last time we used the whole Exception class, which is really bad. We will use only what necessary here.

except TimeoutException as error:
    console_print('failed', '[Login page not OK!]')
    console_print('failed', str(error))
    self._browser.quit()

    raise

We handled the TimeoutException exception here and printed an error message and then raised the error.

Login Attempt

The login_attempt() method will call the login() method and will check if the redirected URL is the dashboard URL or not. Because after login we will be redirected there normally.

self.login(email, password)
redirected_to_dashboard: bool = self.current_url_is_dashboard_url()

And we will print the colored message accordingly.

console_print('success', '[Login is working!]') if 
    redirected_to_dashboard else 
    console_print('failed', '[Wrong credential, Login failed!]')

Now let’s see what the login() method is doing. It’s calling the other methods to set email, password, and submit the form.

self.set_email(email)
self.set_password(password)
self.submit()

We will wait for 2 seconds after that to reload the page properly.

sleep(2)

Set Email, Password And Submit Form

The set_email() method will get the email field and type the given email.

email_field = self._browser.find_element_by_xpath(
    self.login_page_x_paths['email_field'])
email_field.send_keys(email)

Now, here can happen 2 things. One the XPath of the field is wrong or that is not an input field. So 2 kinds of exception can happen. ElementNotInteractableException and NoSuchElementException. Let’s handle them and, print colored messages, quit the browser, and raise the exception.

except (
        ElementNotInteractableException,
        NoSuchElementException) as error:
    console_print('failed', '[Email input failed!]')
    console_print('failed', str(error))
    self._browser.quit()

    raise

The set_password() will do the similar. It will get the password field and type the given password and then submit.

password_field = self._browser.find_element_by_xpath(
    self.login_page_x_paths['password_field'])
password_field.send_keys(password)

And the same kind of exceptions can happen so let’s handle them with,

except (
        ElementNotInteractableException,
        NoSuchElementException) as error:
    console_print('failed', '[Password input failed!]')
    console_print('failed', str(error))
    self._browser.quit()

    raise

The submit() method will click on the submit button. So first we will get the submit button by the given XPath and then click the button.

submit_button = self._browser.find_element_by_xpath(
    self.login_page_x_paths['submit_button'])
submit_button.click()

And to handle the possible exceptions, print colored messages, quit the browser and raise the exception we will do,

except (
        ElementNotInteractableException,
        NoSuchElementException) as error:
    console_print('failed', '[Credential submit failed!]')
    console_print('failed', str(error))
    self._browser.quit()
    print()

    raise

New Tab And Secondary Login Attempt In The Same Browser

The open_and_switch_to_new_tab() will create the new tab and shift to the new tab.

self._browser.execute_script("window.open('');")
sleep(1)
self._browser.switch_to.window(self._browser.window_handles[1])

And the only exception that we must handle is JavascriptException. Because we are executing javascript in the browser.

except JavascriptException as error:
    console_print('failed', '[New tab open failed!]')
    console_print('failed', str(error))
    self._browser.quit()

    raise

in our secondary_login_attempt_in_new_tab() method we will call open_and_switch_to_new_tab() method to create new tab and shift focus there then we will call visit_login_page() to go to the login URL. But if everything is fine then we will be redirected to the dashboard URL. To check that we will call the current_url_is_dashboard_url() method which will actually return boolean.

self.open_and_switch_to_new_tab()
self.visit_login_page()
redirected_to_dashboard: bool = self.current_url_is_dashboard_url()
console_print('success', '[Secondary login attempt redirected to '
                         'dashboard!]') if 
    redirected_to_dashboard else 
    console_print('failed', '[New tab redirection failed!]')

Congratulation if you understood all the codes. Its time to execute the code finally.

[ Step:06 ] It’s Time, Run The Automation

In our core package application, we will run the login automation testing. Write the below codes in the application/app.py file.

"""Main application test execution area"""

from homepage.homepage import HomepageTest
from application.utils.urls import HOMEPAGE_URL, LOGIN_URL, ADMIN_DASHBOARD
from login.login import LoginTest
from application.utils.constants import HOMEPAGE_NAV_BAR, LOGIN_PAGE_X_PATHS, 
    DASHBOARD_UPPER_RIGHT_MENU_X_PATH, LOGOUT_LINK_X_PATH
from settings import ADMIN_EMAIL, ADMIN_PASSWORD, WRONG_EMAIL, WRONG_PASSWORD


if __name__ == '__main__':

    # Homepage Testing
    homepage = HomepageTest(HOMEPAGE_URL, HOMEPAGE_NAV_BAR)
    homepage.visit_homepage()
    homepage.nav_bar_content_testing()
    homepage.click_nav_elements_on_fullscreen()
    homepage.click_nav_elements_on_mobile_screen()
    homepage.close_browser()

    # Successful Login Testing
    login_test = LoginTest(LOGIN_URL, LOGIN_PAGE_X_PATHS, ADMIN_DASHBOARD)
    login_test.visit_login_page()
    login_test.login_attempt(ADMIN_EMAIL, ADMIN_PASSWORD)
    login_test.secondary_login_attempt_in_new_tab()
    login_test.logout(DASHBOARD_UPPER_RIGHT_MENU_X_PATH, LOGOUT_LINK_X_PATH)

    # Failed Login Testing
    login_test.visit_login_page()
    login_test.login_attempt(WRONG_EMAIL, WRONG_PASSWORD)

Explanation

We also have our old homepage testing execution code here. Below the homepage testing code, We create an object of LoginTest Class with LOGIN_URL, LOGIN_PAGE_X_PATHS, ADMIN_DASHBOARD. To test user login you have to give USER_DASHBOARD here.

login_test = LoginTest(LOGIN_URL, LOGIN_PAGE_X_PATHS, ADMIN_DASHBOARD)

Then we will call visit_login_page() to visit the homepage and try to attempt a login request by login_attempt() method where we are giving two parameters called ADMIN_EMAIL and ADMIN_PASSWORD, which we imported from settings.py module.

login_test.visit_login_page()
login_test.login_attempt(ADMIN_EMAIL, ADMIN_PASSWORD)

Then we will try to open a new tab to log in again, which is not theoretically possible but we need to check if it’s redirecting us t the dashboard or not.

login_test.secondary_login_attempt_in_new_tab()

Then we will do the logout.

login_test.logout(DASHBOARD_UPPER_RIGHT_MENU_X_PATH, LOGOUT_LINK_X_PATH)

After that, we will try to login with wrong credentials. And definitely we will fail!!!

# Failed Login Testing
login_test.visit_login_page()
login_test.login_attempt(WRONG_EMAIL, WRONG_PASSWORD)

Here is a video demo of all the procedures.

Conclusion

Congratulation, We have automated the login. Keep watching the series for more content. In our next episode will do some awesome functional testing. Share this article with your friends to help them learn the web automation. Thanks for being with me for the whole time.