Test a Flask App with Selenium WebDriver - Part 1

Mbithe Nzomo
👁️ 7,176 views
💬 comments

Ever wondered how to write tests for the front-end of your web application? You may already have functional back-end tests, for example to ensure that your models and views are working. However, you may be unsure how to simulate a user of your app for testing. How can you test front-end functions like registration and logging in, which are done in a browser?

In this two-part tutorial, I will show you how to write front-end tests for an existing Python/Flask web application. You should therefore already have a functional application, along with a virtual environment with the necessary software dependencies installed. We will use Project Dream Team, a CRUD web app I built in a three-part tutorial (here is Part One, Part Two and Part Three). I recommend that you go through the tutorial and build out the app, especially if you are new to Flask or to testing in Flask. If you have built and tested a Flask app before, or don't want to go through the tutorial, you can clone the complete app from my GitHub repository, here and follow the set-up instructions in the README file.

Testing Overview

To recap, the application we will be testing is an employee management app. The app allows users to register as employees and login. It also allows admin users to view all employees, as well as to create departments and roles, and then assign them to each employee.

Table of Contents

    We will therefore write tests to ensure all these functions are achieved in the app. If you followed the tutorial, you should already have a test database set up. If not, you can create one and grant your database user privileges on it, as follows:

    $ mysql -u root
    
    mysql> CREATE DATABASE dreamteam_test;
    Query OK, 1 row affected (0.00 sec)
    
    mysql> GRANT ALL PRIVILEGES ON dreamteam_test . * TO 'dt_admin'@'localhost';
    Query OK, 0 rows affected (0.00 sec)

    We use the test database to run our tests to prevent our test data from being saved into the main database we are using for development.

    The back-end tests, which were already written in the app tutorial, test that the app's models and views are working as expected. The front-end tests, which we will write in this tutorial, will simulate a user using the app in the browser. For these tests, we will create test data, and then test the registration, login, and CRUD functionality of the app.

    Test Directory Structure

    We previously had only one test file, tests.py, in the root directory. Rather than have all the tests in one file, we shall create a tests directory. In it, create two new files: __init__.py and test_front_end.py. Move the tests.py file to the tests directory and rename it to test_back_end.py. Your tests directory should now look like this:

    └── tests
        ├── __init__.py
        ├── test_back_end.py
        └── test_front_end.py

    The __init__.py file initializes the tests directory as a Python package. test-back_end.py will contain all our back-end tests, while test_front_end.py will contain all our front-end tests. Update the test_back_end.py file as follows:

    # tests/test_back_end.py
    
    import unittest
    
    from flask import abort, url_for
    from flask_testing import TestCase
    
    from app import create_app, db
    from app.models import Department, Employee, Role
    
    class TestBase(TestCase):
    
        def create_app(self):
    
            # pass in test configurations
            config_name = 'testing'
            app = create_app(config_name)
            app.config.update(
                SQLALCHEMY_DATABASE_URI='mysql://dt_admin:dt2016@localhost/dreamteam_test'
            )
            return app
    
        def setUp(self):
            """
            Will be called before every test
            """
    
            db.session.commit()
            db.drop_all()
            db.create_all()
    
            # create test admin user
            admin = Employee(username="admin", password="admin2016", is_admin=True)
    
            # create test non-admin user
            employee = Employee(username="test_user", password="test2016")
    
            # save users to database
            db.session.add(admin)
            db.session.add(employee)
            db.session.commit()
    
        def tearDown(self):
            """
            Will be called after every test
            """
    
            db.session.remove()
            db.drop_all()
    
    class TestModels(TestBase):
    
        def test_employee_model(self):
            """
            Test number of records in Employee table
            """
            self.assertEqual(Employee.query.count(), 2)
    
        def test_department_model(self):
            """
            Test number of records in Department table
            """
    
            # create test department
            department = Department(name="IT", description="The IT Department")
    
            # save department to database
            db.session.add(department)
            db.session.commit()
    
            self.assertEqual(Department.query.count(), 1)
    
        def test_role_model(self):
            """
            Test number of records in Role table
            """
    
            # create test role
            role = Role(name="CEO", description="Run the whole company")
    
            # save role to database
            db.session.add(role)
            db.session.commit()
    
            self.assertEqual(Role.query.count(), 1)
    
    class TestViews(TestBase):
    
        def test_homepage_view(self):
            """
            Test that homepage is accessible without login
            """
            response = self.client.get(url_for('home.homepage'))
            self.assertEqual(response.status_code, 200)
    
        def test_login_view(self):
            """
            Test that login page is accessible without login
            """
            response = self.client.get(url_for('auth.login'))
            self.assertEqual(response.status_code, 200)
    
        def test_logout_view(self):
            """
            Test that logout link is inaccessible without login
            and redirects to login page then to logout
            """
            target_url = url_for('auth.logout')
            redirect_url = url_for('auth.login', next=target_url)
            response = self.client.get(target_url)
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, redirect_url)
    
        def test_dashboard_view(self):
            """
            Test that dashboard is inaccessible without login
            and redirects to login page then to dashboard
            """
            target_url = url_for('home.dashboard')
            redirect_url = url_for('auth.login', next=target_url)
            response = self.client.get(target_url)
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, redirect_url)
    
        def test_admin_dashboard_view(self):
            """
            Test that dashboard is inaccessible without login
            and redirects to login page then to dashboard
            """
            target_url = url_for('home.admin_dashboard')
            redirect_url = url_for('auth.login', next=target_url)
            response = self.client.get(target_url)
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, redirect_url)
    
        def test_departments_view(self):
            """
            Test that departments page is inaccessible without login
            and redirects to login page then to departments page
            """
            target_url = url_for('admin.list_departments')
            redirect_url = url_for('auth.login', next=target_url)
            response = self.client.get(target_url)
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, redirect_url)
    
        def test_roles_view(self):
            """
            Test that roles page is inaccessible without login
            and redirects to login page then to roles page
            """
            target_url = url_for('admin.list_roles')
            redirect_url = url_for('auth.login', next=target_url)
            response = self.client.get(target_url)
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, redirect_url)
    
        def test_employees_view(self):
            """
            Test that employees page is inaccessible without login
            and redirects to login page then to employees page
            """
            target_url = url_for('admin.list_employees')
            redirect_url = url_for('auth.login', next=target_url)
            response = self.client.get(target_url)
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, redirect_url)
    
    class TestErrorPages(TestBase):
    
        def test_403_forbidden(self):
            # create route to abort the request with the 403 Error
            @self.app.route('/403')
            def forbidden_error():
                abort(403)
    
            response = self.client.get('/403')
            self.assertEqual(response.status_code, 403)
            self.assertTrue("403 Error" in response.data)
    
        def test_404_not_found(self):
            response = self.client.get('/nothinghere')
            self.assertEqual(response.status_code, 404)
            self.assertTrue("404 Error" in response.data)
    
        def test_500_internal_server_error(self):
            # create route to abort the request with the 500 Error
            @self.app.route('/500')
            def internal_server_error():
                abort(500)
    
            response = self.client.get('/500')
            self.assertEqual(response.status_code, 500)
            self.assertTrue("500 Error" in response.data)
    
    if __name__ == '__main__':
        unittest.main()

    Because we now have multiple test files, we need a way to run all the tests at once. It would be inconvenient to have to run each test file individually. For this we will use nose2, a package that extends Python's unit testing framework, unittest, and makes testing easier.

    $ pip install nose2

    Now, we will run the tests using the nose2 command. This command looks for all files whose names begin with "test", and runs the methods inside these files whose names begin with "test". This means that if you name your test files and methods incorrectly, they will not be run by nose2. First, remember to ensure that your MySQL server is running. You can do this by running the following command:

    $ mysqld

    Then, in another terminal window, run the tests:

    $ nose2
    ..............
    ----------------------------------------------------------------------
    Ran 14 tests in 2.582s
    
    OK

    Introduction to Selenium WebDriver

    Selenium is a suite of tools to automate web browsers for a variety of purposes. Selenium WebDriver in particular is useful when writing browser-based tests.

    Begin by installing Selenium:

    $ pip install selenium

    Selenium Set-Up

    Now, let's set up Selenium. We are using Flask Testing, an extension that provides unit testing utilities for Flask. To use Selenium with Flask Testing, we need to inherit from the LiveServerTestCase class, which will allow us to run a live server during our tests.

    We will use ChromeDriver, a WebDriver for Chrome. This will allow us to run the front-end tests on the Chrome browser. If you are on MacOS, you can install it simply using the brew install chromedriver command. For other platforms, follow this Getting Started guide to download and properly set up ChromeDriver. Note that you will need to have Chrome browser installed.

    If you prefer to use Firefox broswer for your tests, you can install and set up FirefoxDriver instead.

    For the front-end tests, we will begin by creating test data that will be added to the database before each test runs. This includes two test users, two test departments, and two test roles. Add the following code to tests/test_front_end.py:

    # tests/front_end_tests.py
    
    import unittest
    import urllib2
    
    from flask_testing import LiveServerTestCase
    from selenium import webdriver
    
    from app import create_app, db
    from app.models import Employee, Role, Department
    
    # Set test variables for test admin user
    test_admin_username = "admin"
    test_admin_email = "admin@email.com"
    test_admin_password = "admin2016"
    
    # Set test variables for test employee 1
    test_employee1_first_name = "Test"
    test_employee1_last_name = "Employee"
    test_employee1_username = "employee1"
    test_employee1_email = "employee1@email.com"
    test_employee1_password = "1test2016"
    
    # Set test variables for test employee 2
    test_employee2_first_name = "Test"
    test_employee2_last_name = "Employee"
    test_employee2_username = "employee2"
    test_employee2_email = "employee2@email.com"
    test_employee2_password = "2test2016"
    
    # Set variables for test department 1
    test_department1_name = "Human Resources"
    test_department1_description = "Find and keep the best talent"
    
    # Set variables for test department 2
    test_department2_name = "Information Technology"
    test_department2_description = "Manage all tech systems and processes"
    
    # Set variables for test role 1
    test_role1_name = "Head of Department"
    test_role1_description = "Lead the entire department"
    
    # Set variables for test role 2
    test_role2_name = "Intern"
    test_role2_description = "3-month learning position"
    
    class TestBase(LiveServerTestCase):
    
        def create_app(self):
            config_name = 'testing'
            app = create_app(config_name)
            app.config.update(
                # Specify the test database
                SQLALCHEMY_DATABASE_URI='mysql://dt_admin:dt2016@localhost/dreamteam_test',
                # Change the port that the liveserver listens on
                LIVESERVER_PORT=8943
            )
            return app
    
        def setUp(self):
            """Setup the test driver and create test users"""
            self.driver = webdriver.Chrome()
            self.driver.get(self.get_server_url())
    
            db.session.commit()
            db.drop_all()
            db.create_all()
    
            # create test admin user
            self.admin = Employee(username=test_admin_username,
                                  email=test_admin_email,
                                  password=test_admin_password,
                                  is_admin=True)
    
            # create test employee user
            self.employee = Employee(username=test_employee1_username,
                                     first_name=test_employee1_first_name,
                                     last_name=test_employee1_last_name,
                                     email=test_employee1_email,
                                     password=test_employee1_password)
    
            # create test department
            self.department = Department(name=test_department1_name,
                                         description=test_department1_description)
    
            # create test role
            self.role = Role(name=test_role1_name,
                             description=test_role1_description)
    
            # save users to database
            db.session.add(self.admin)
            db.session.add(self.employee)
            db.session.add(self.department)
            db.session.add(self.role)
            db.session.commit()
    
        def tearDown(self):
            self.driver.quit()
    
        def test_server_is_up_and_running(self):
            response = urllib2.urlopen(self.get_server_url())
            self.assertEqual(response.code, 200)
    
    if __name__ == '__main__':
        unittest.main()

    At the beginning of the file, right after the imports, we begin by creating test variables which we will use in the tests. These include details for three test users, one admin and the other two non-admin, as well as two test departments and two test roles.

    Next, we create a TestBase class, which subsequent test classes will inherit from. In it, we have the create_app method, which Flask Testing requires to return an app instance. In this method, we specify the Flask configuration and test database, just like we did in the back-end tests. We also specify the port that the live server will run on. Flask uses port 5000 by default. Changing this for the live server will ensure that the tests run on another port (in this case, port 8943), thus preventing any conflict with another Flask app that could be running.

    In the setUp method, we set up the test driver as the ChromeDriver. (If using another webdriver, such as Firefox for example, use self.driver = webdriver.Firefox().) Take note of the self.get_server_url method; we use this to get the home URL. We also create a test employee, test admin user, test department and test role using the variables we created initially. In the tearDown method, we quit the webdriver, and clear the test database. The setUp method is called before every test, while the tearDown method is called after each test. It is important to have the test data before the tests run because most of our tests will require existing users, departments and roles. It would be repetitive to create new users, departments and roles for each test, so it is simpler and DRY-er to do it in the setUp method.

    We have one test method in the base class, test_server_is_up_and_running. This method simply ensures that the test server is working as expected and returns a 200 OK status code.

    Run the tests using the nose2 command:

    $ nose2
    ...............
    ----------------------------------------------------------------------
    Ran 15 tests in 4.313s
    
    OK

    It shows that it ran 15 tests, which includes the 14 back-end tests that we had initially, as well as 1 front-end test.

    Finding Elements

    So far, we have only one test in the test_front_end.py file. We need to write additional tests that simulate a user actually using the app, such as clicking links and buttons, entering data into forms, and so on. To tell Selenium to click a particular button or enter data in a particular field in a form, we need to use unique identifiers for these web elements.

    Selenium provides methods that we can call to find web elements in a variety of ways, such as by id, by name, by XPath, and by CSS selector. I find that locating an element by id is one of the simplest ways; it only requires you to give an id attribute to the element that needs to be located.

    We'll start by assigning id attributes to the menu links in the app so that we can direct the test user to click them as required. Open the templates/base.html file and update the menu items with id attributes as follows:

    <!-- app/templates/base.html -->
    
    <!-- Modify nav bar menu -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
        <ul class="nav navbar-nav navbar-right">
            {% if current_user.is_authenticated %}
              {% if current_user.is_admin %}
                <li id="dashboard_link_admin"><a href="{{ url_for('home.admin_dashboard') }}">Dashboard</a></li>
                <li id="departments_link"><a href="{{ url_for('admin.list_departments') }}">Departments</a></li>
                <li id="roles_link"><a href="{{ url_for('admin.list_roles') }}">Roles</a></li>
                <li id="employees_link"><a href="{{ url_for('admin.list_employees') }}">Employees</a></li>
              {% else %}
                <li id="dashboard_link_employee"><a href="{{ url_for('home.dashboard') }}">Dashboard</a></li>
              {% endif %}
              <li id="logout_link"><a href="{{ url_for('auth.logout') }}">Logout</a></li>
              <li id="username_greeting"><a><i class="fa fa-user"></i>  Hi, {{ current_user.username }}!</a></li>
            {% else %}
              <li id="home_link"><a href="{{ url_for('home.homepage') }}">Home</a></li>
              <li id="register_link"><a href="{{ url_for('auth.register') }}">Register</a></li>
              <li id="login_link"><a href="{{ url_for('auth.login') }}">Login</a></li>
            {% endif %}
        </ul>
    </div>

    Most other web elements that we will need to locate already have id attributes. For those that don't, we will find them by their CSS class names and selectors.

    Testing the Auth Blueprint

    We will begin by writing tests for the Auth blueprint, which handles registration, login, and logout. These tests need to handle edge cases, such as users entering invalid data, so that we can ensure the app responds appropriately.

    Registration Tests

    We'll start by writing the registration tests. Add the following code to the test_front_end.py file, after the TestBase class:

    # tests/front_end_tests.py
    
    # update imports
    import time
    from flask import url_for
    
    class TestRegistration(TestBase):
    
        def test_registration(self):
            """
            Test that a user can create an account using the registration form
            if all fields are filled out correctly, and that they will be 
            redirected to the login page
            """
    
            # Click register menu link
            self.driver.find_element_by_id("register_link").click()
            time.sleep(1)
    
            # Fill in registration form
            self.driver.find_element_by_id("email").send_keys(test_employee2_email)
            self.driver.find_element_by_id("username").send_keys(
                test_employee2_username)
            self.driver.find_element_by_id("first_name").send_keys(
                test_employee2_first_name)
            self.driver.find_element_by_id("last_name").send_keys(
                test_employee2_last_name)
            self.driver.find_element_by_id("password").send_keys(
                test_employee2_password)
            self.driver.find_element_by_id("confirm_password").send_keys(
                test_employee2_password)
            self.driver.find_element_by_id("submit").click()
            time.sleep(1)
    
            # Assert that browser redirects to login page
            assert url_for('auth.login') in self.driver.current_url
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully registered" in success_message
    
            # Assert that there are now 3 employees in the database
            self.assertEqual(Employee.query.count(), 3)
    
        def test_registration_invalid_email(self):
            """
            Test that a user cannot register using an invalid email format
            and that an appropriate error message will be displayed
            """
    
            # Click register menu link
            self.driver.find_element_by_id("register_link").click()
            time.sleep(1)
    
            # Fill in registration form
            self.driver.find_element_by_id("email").send_keys("invalid_email")
            self.driver.find_element_by_id("username").send_keys(
                test_employee2_username)
            self.driver.find_element_by_id("first_name").send_keys(
                test_employee2_first_name)
            self.driver.find_element_by_id("last_name").send_keys(
                test_employee2_last_name)
            self.driver.find_element_by_id("password").send_keys(
                test_employee2_password)
            self.driver.find_element_by_id("confirm_password").send_keys(
                test_employee2_password)
            self.driver.find_element_by_id("submit").click()
            time.sleep(5)
    
            # Assert error message is shown
            error_message = self.driver.find_element_by_class_name(
                "help-block").text
            assert "Invalid email address" in error_message
    
        def test_registration_confirm_password(self):
            """
            Test that an appropriate error message is displayed when the password 
            and confirm_password fields do not match
            """
    
            # Click register menu link
            self.driver.find_element_by_id("register_link").click()
            time.sleep(1)
    
            # Fill in registration form
            self.driver.find_element_by_id("email").send_keys(test_employee2_email)
            self.driver.find_element_by_id("username").send_keys(
                test_employee2_username)
            self.driver.find_element_by_id("first_name").send_keys(
                test_employee2_first_name)
            self.driver.find_element_by_id("last_name").send_keys(
                test_employee2_last_name)
            self.driver.find_element_by_id("password").send_keys(
                test_employee2_password)
            self.driver.find_element_by_id("confirm_password").send_keys(
                "password-won't-match")
            self.driver.find_element_by_id("submit").click()
            time.sleep(5)
    
            # Assert error message is shown
            error_message = self.driver.find_element_by_class_name(
                "help-block").text
            assert "Field must be equal to confirm_password" in error_message

    We've created a class, TestRegistration, that inherits from the TestBase class, and has three test methods. Note that the if __name__ == '__main__': section in the file should always be at the bottom of the file, so any new code additions will go before it.

    The test_registration method tests that a user can successfully create an account if they fill out all fields in the registration form correctly. It also tests that the user is redirected to the login page after registration and that a message is displayed inviting them to login. The test_registration_invalid_email method tests that entering an invalid email format in the email field will prevent the registration form from being submitted, and display an appropriate error message. The test_registration_confirm_password method tests that the data in the "Password" and "Confirm password" fields must match, and that an appropriate error message is displayed if they do not.

    Take note of the send_keys method, which we use to enter data into form fields, and the click method, which we use to click on web elements such as buttons and links. Additionally, take note of the time.sleep method. We use this to pause the tests to give the browser time to load all web elements completely before proceeding to the next step.

    Login Tests

    The next tests are for logging in. Add a TestLogin class as follows:

    # tests/front_end_tests.py
    
    class TestLogin(TestBase):
    
        def test_login(self):
            """
            Test that a user can login and that they will be redirected to
            the homepage
            """
    
            # Click login menu link
            self.driver.find_element_by_id("login_link").click()
            time.sleep(1)
    
            # Fill in login form
            self.driver.find_element_by_id("email").send_keys(test_employee1_email)
            self.driver.find_element_by_id("password").send_keys(
                test_employee1_password)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert that browser redirects to dashboard page
            assert url_for('home.dashboard') in self.driver.current_url
    
            # Assert that welcome greeting is shown
            username_greeting = self.driver.find_element_by_id(
                "username_greeting").text
            assert "Hi, employee1!" in username_greeting
    
        def test_admin_login(self):
            """
            Test that an admin user can login and that they will be redirected to
            the admin homepage
            """
    
            # Click login menu link
            self.driver.find_element_by_id("login_link").click()
            time.sleep(1)
    
            # Fill in login form
            self.driver.find_element_by_id("email").send_keys(test_admin_email)
            self.driver.find_element_by_id("password").send_keys(
                test_admin_password)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert that browser redirects to dashboard page
            assert url_for('home.admin_dashboard') in self.driver.current_url
    
            # Assert that welcome greeting is shown
            username_greeting = self.driver.find_element_by_id(
                "username_greeting").text
            assert "Hi, admin!" in username_greeting
    
        def test_login_invalid_email_format(self):
            """
            Test that a user cannot login using an invalid email format
            and that an appropriate error message will be displayed
            """
    
            # Click login menu link
            self.driver.find_element_by_id("login_link").click()
            time.sleep(1)
    
            # Fill in login form
            self.driver.find_element_by_id("email").send_keys("invalid")
            self.driver.find_element_by_id("password").send_keys(
                test_employee1_password)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert error message is shown
            error_message = self.driver.find_element_by_class_name(
                "help-block").text
            assert "Invalid email address" in error_message
    
        def test_login_wrong_email(self):
            """
            Test that a user cannot login using the wrong email
            and that an appropriate error message will be displayed
            """
    
            # Click login menu link
            self.driver.find_element_by_id("login_link").click()
            time.sleep(1)
    
            # Fill in login form
            self.driver.find_element_by_id("email").send_keys(test_employee2_email)
            self.driver.find_element_by_id("password").send_keys(
                test_employee1_password)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert that error message is shown
            error_message = self.driver.find_element_by_class_name("alert").text
            assert "Invalid email or password" in error_message
    
        def test_login_wrong_password(self):
            """
            Test that a user cannot login using the wrong password
            and that an appropriate error message will be displayed
            """
    
            # Click login menu link
            self.driver.find_element_by_id("login_link").click()
            time.sleep(1)
    
            # Fill in login form
            self.driver.find_element_by_id("email").send_keys(test_employee1_email)
            self.driver.find_element_by_id("password").send_keys(
                "invalid")
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert that error message is shown
            error_message = self.driver.find_element_by_class_name("alert").text
            assert "Invalid email or password" in error_message

    The test_login method tests that when the correct email and password combination is entered for a registered user, the user is redirected to the dashboard, where their username is displayed. It is the same as the test_admin_login method, except the latter tests login for admin users who are redirected to the admin dashboard. The test_invalid_email_format tests that users cannot submit the login form if they enter a non-email in the email field. The test_login_wrong_email and test_login_wrong_password methods test that a user cannot login using invalid credentials, and that an appropriate error message is displayed.

    Run the tests again. You'll notice that for the front-end tests, a browser window will open. It will simulate a user clicking links and filling out forms as specified in the tests we wrote. The output of running the tests should be similar to this:

    $ nose2
    ........................
    ----------------------------------------------------------------------
    Ran 25 tests in 96.581s
    
    OK

    Testing the Admin Blueprint

    Next, we will write tests for the Admin blueprint, which is where the CRUD functionality of the application is. In these tests, we will simulate an admin user creating, viewing, updating, and deleting departments and roles. We will also simulate an admin user assigning departments and roles to employees.

    Because we will need to log in multiple times before being able to do any of this, we will create a method that we will call every time we need to log in as a user.

    In your front_end_tests.py file, add a CreateObjects class, just after the test variables and before the TestRegistration class:

    # tests/front_end_tests.py
    
    # after initialization of test variables
    
    class CreateObjects(object):
    
        def login_admin_user(self):
            """Log in as the test employee"""
            login_link = self.get_server_url() + url_for('auth.login')
            self.driver.get(login_link)
            self.driver.find_element_by_id("email").send_keys(test_admin_email)
            self.driver.find_element_by_id("password").send_keys(
                test_admin_password)
            self.driver.find_element_by_id("submit").click()
    
        def login_test_user(self):
            """Log in as the test employee"""
            login_link = self.get_server_url() + url_for('auth.login')
            self.driver.get(login_link)
            self.driver.find_element_by_id("email").send_keys(test_employee1_email)
            self.driver.find_element_by_id("password").send_keys(
                test_employee1_password)
            self.driver.find_element_by_id("submit").click()

    Now, if we need to be logged in as a particular type of user in any of our tests, we can simply call the relevant method.

    Department Tests

    Next, we'll write our tests. Add a TestDepartments class after the TestLogin class, as follows:

    # tests/front_end_tests.py
    
    class TestDepartments(CreateObjects, TestBase):
    
        def test_add_department(self):
            """
            Test that an admin user can add a department
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click departments menu link
            self.driver.find_element_by_id("departments_link").click()
            time.sleep(1)
    
            # Click on add department button
            self.driver.find_element_by_class_name("btn").click()
            time.sleep(1)
    
            # Fill in add department form
            self.driver.find_element_by_id("name").send_keys(test_department2_name)
            self.driver.find_element_by_id("description").send_keys(
                test_department2_description)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully added a new department" in success_message
    
            # Assert that there are now 2 departments in the database
            self.assertEqual(Department.query.count(), 2)
    
        def test_add_existing_department(self):
            """
            Test that an admin user cannot add a department with a name
            that already exists
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click departments menu link
            self.driver.find_element_by_id("departments_link").click()
            time.sleep(1)
    
            # Click on add department button
            self.driver.find_element_by_class_name("btn").click()
            time.sleep(1)
    
            # Fill in add department form
            self.driver.find_element_by_id("name").send_keys(test_department1_name)
            self.driver.find_element_by_id("description").send_keys(
                test_department1_description)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert error message is shown
            error_message = self.driver.find_element_by_class_name("alert").text
            assert "Error: department name already exists" in error_message
    
            # Assert that there is still only 1 department in the database
            self.assertEqual(Department.query.count(), 1)
    
        def test_edit_department(self):
            """
            Test that an admin user can edit a department
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click departments menu link
            self.driver.find_element_by_id("departments_link").click()
            time.sleep(1)
    
            # Click on edit department link
            self.driver.find_element_by_class_name("fa-pencil").click()
            time.sleep(1)
    
            # Fill in add department form
            self.driver.find_element_by_id("name").clear()
            self.driver.find_element_by_id("name").send_keys("Edited name")
            self.driver.find_element_by_id("description").clear()
            self.driver.find_element_by_id("description").send_keys(
                "Edited description")
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully edited the department" in success_message
    
            # Assert that department name and description has changed
            department = Department.query.get(1)
            self.assertEqual(department.name, "Edited name")
            self.assertEqual(department.description, "Edited description")
    
        def test_delete_department(self):
            """
            Test that an admin user can delete a department
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click departments menu link
            self.driver.find_element_by_id("departments_link").click()
            time.sleep(1)
    
            # Click on edit department link
            self.driver.find_element_by_class_name("fa-trash").click()
            time.sleep(1)
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully deleted the department" in success_message
    
            # Assert that there are no departments in the database
            self.assertEqual(Department.query.count(), 0)

    The test_add_department method tests that an admin user can add a department using the add department form, while the test_add_existing_department method tests that they cannot add a department name that already exists. The test_edit_department method tests that a department can be edited using the edit form. Take note of the clear method, which allows us to clear a textbox of any exisiting text before entering some other text. Lastly, the test_delete_department tests than a department can be deleted using the delete link in the departments page.

    Role Tests

    We will write similar tests for roles:

    # tests/front_end_tests.py
    
    class TestRoles(CreateObjects, TestBase):
    
        def test_add_role(self):
            """
            Test that an admin user can add a role
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click roles menu link
            self.driver.find_element_by_id("roles_link").click()
            time.sleep(1)
    
            # Click on add role button
            self.driver.find_element_by_class_name("btn").click()
            time.sleep(1)
    
            # Fill in add role form
            self.driver.find_element_by_id("name").send_keys(test_role2_name)
            self.driver.find_element_by_id("description").send_keys(
                test_role2_description)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully added a new role" in success_message
    
            # Assert that there are now 2 roles in the database
            self.assertEqual(Role.query.count(), 2)
    
        def test_add_existing_role(self):
            """
            Test that an admin user cannot add a role with a name
            that already exists
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click roles menu link
            self.driver.find_element_by_id("roles_link").click()
            time.sleep(1)
    
            # Click on add role button
            self.driver.find_element_by_class_name("btn").click()
            time.sleep(1)
    
            # Fill in add role form
            self.driver.find_element_by_id("name").send_keys(test_role1_name)
            self.driver.find_element_by_id("description").send_keys(
                test_role1_description)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert error message is shown
            error_message = self.driver.find_element_by_class_name("alert").text
            assert "Error: role name already exists" in error_message
    
            # Assert that there is still only 1 role in the database
            self.assertEqual(Role.query.count(), 1)
    
        def test_edit_role(self):
            """
            Test that an admin user can edit a role
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click roles menu link
            self.driver.find_element_by_id("roles_link").click()
            time.sleep(1)
    
            # Click on edit role link
            self.driver.find_element_by_class_name("fa-pencil").click()
            time.sleep(1)
    
            # Fill in add role form
            self.driver.find_element_by_id("name").clear()
            self.driver.find_element_by_id("name").send_keys("Edited name")
            self.driver.find_element_by_id("description").clear()
            self.driver.find_element_by_id("description").send_keys(
                "Edited description")
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully edited the role" in success_message
    
            # Assert that role name and description has changed
            role = Role.query.get(1)
            self.assertEqual(role.name, "Edited name")
            self.assertEqual(role.description, "Edited description")
    
        def test_delete_role(self):
            """
            Test that an admin user can delete a role
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click roles menu link
            self.driver.find_element_by_id("roles_link").click()
            time.sleep(1)
    
            # Click on edit role link
            self.driver.find_element_by_class_name("fa-trash").click()
            time.sleep(1)
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully deleted the role" in success_message
    
            # Assert that there are no roles in the database
            self.assertEqual(Role.query.count(), 0)

    Exception Handling

    You may also need to edit the add_department and add_role views to handle an SQLAlchemy exception that may occur during transaction rollback when attempting to add a department or role that already exists. To handle this exception, edit the try-except block in the add_department and add_role views, as follows:

    # app/admin/views.py
    
    def add_department(self):
    
        # existing code remains
    
        if form.validate_on_submit():
            department = Department(name=form.name.data,
                                    description=form.description.data)
            try:
                # add department to the database
                db.session.add(department)
                db.session.commit()
                flash('You have successfully added a new department.')
            except:
                # in case department name already exists
                db.session.rollback()
                flash('Error: department name already exists.')
    
    def add_role(self):
    
        # existing code remains
    
       if form.validate_on_submit():
            role = Role(name=form.name.data,
                        description=form.description.data)
    
            try:
                # add role to the database
                db.session.add(role)
                db.session.commit()
                flash('You have successfully added a new role.')
            except:
                # in case role name already exists
                db.session.rollback()
                flash('Error: role name already exists.')

    Run your tests now:

    $ nose2
    ...................................
    ----------------------------------------------------------------------
    Ran 35 tests in 234.457s
    
    OK

    Employee Tests

    Now we will write tests where the admin user assigns departments and roles to employees.

    # tests/front_end_tests.py
    
    # update imports
    from selenium.webdriver.support.ui import Select
    
    class TestEmployees(CreateObjects, TestBase):
    
        def test_assign(self):
            """
            Test that an admin user can assign a role and a department
            to an employee
            """
    
            # Login as admin user
            self.login_admin_user()
    
            # Click employees menu link
            self.driver.find_element_by_id("employees_link").click()
            time.sleep(1)
    
            # Click on assign link
            self.driver.find_element_by_class_name("fa-user-plus").click()
            time.sleep(1)
    
            # Department and role already loaded in form
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully assigned a department and role" in success_message
    
            # Assert that department and role has been assigned to employee
            employee = Employee.query.get(2)
            self.assertEqual(employee.role.name, test_role1_name)
            self.assertEqual(employee.department.name, test_department1_name)
    
        def test_reassign(self):
            """
            Test that an admin user can assign a new role and a new department
            to an employee
            """
    
            # Create new department
            department = Department(name=test_department2_name,
                                    description=test_department2_description)
    
            # Create new role
            role = Role(name=test_role2_name,
                        description=test_role2_description)
    
            # Add to database
            db.session.add(department)
            db.session.add(role)
            db.session.commit()
    
            # Login as admin user
            self.login_admin_user()
    
            # Click employees menu link
            self.driver.find_element_by_id("employees_link").click()
            time.sleep(1)
    
            # Click on assign link
            self.driver.find_element_by_class_name("fa-user-plus").click()
            time.sleep(1)
    
            # Select new department and role
            select_dept = Select(self.driver.find_element_by_id("department"))
            select_dept.select_by_visible_text(test_department2_name)
            select_role = Select(self.driver.find_element_by_id("role"))
            select_role.select_by_visible_text(test_role2_name)
            self.driver.find_element_by_id("submit").click()
            time.sleep(2)
    
            # Assert success message is shown
            success_message = self.driver.find_element_by_class_name("alert").text
            assert "You have successfully assigned a department and role" in success_message
    
            # Assert that employee's department and role has changed
            employee = Employee.query.get(2)
            self.assertEqual(employee.role.name, test_role2_name)
            self.assertEqual(employee.department.name, test_department2_name)

    The test_assign method assigns the exisiting department and role to the existing user using the assign link in the Employees page. The test_reassign method adds a new department and role to the database, and then assigns them to the existing employee. Take note of the Select class that we have imported from Selenium. From it, we use the select_by_visible_text method to select the department and role from a dropdown menu.

    Let's run the tests one more time:

    $ nose2
    ......................................
    ----------------------------------------------------------------------
    Ran 38 tests in 181.709s
    
    OK

    Conclusion

    That's it for Part One! To recap, in this tutorial you've learnt about Selenium WebDriver and how it can be used to run front-end tests by simulating a user of your app. You've also learnt about web elements and how to find them using some of Selenium's in-built methods. We have written tests for registration, login, and performing CRUD operations on departments and roles.

    In Part Two, we will write tests for permissions, to ensure that only authorised users can access certain resources. Part Two will also cover continuous integration and linking our app with CircleCI, a continuous integration and delivery platform.

    Mbithe Nzomo

    8 posts

    I am a software developer. I work primarily with Python, and have experience with frameworks such as Django and Flask. I consider myself a limitless learner, and love learning new tools and technologies that make me a better developer. I am excited about travelling, good music, and increasing diversity in the tech industry.