This tutorial is out of date and no longer maintained.
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 previous three-part tutorial.
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.
To recap, the application we will be testing is an employee management app. The app allows users to register as employees and log in. It also allows admin users to view all employees, as well as to create departments and roles, and then assign them to each employee.
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
- CREATE DATABASE dreamteam_test;
Query OK, 1 row affected (0.00 sec)
- 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.
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
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
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 browser 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@exmple.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@example.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@example.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.
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.
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.
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 log in. 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.
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 log in 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
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.
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 existing text before entering some other text. Lastly, the test_delete_department
tests that a department can be deleted using the delete link on the departments page.
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)
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
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 existing department and role to the existing user using the assign link on 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
That’s it for Part One! To recap, in this tutorial you’ve learned about Selenium WebDriver and how it can be used to run front-end tests by simulating a user of your app. You’ve also learned 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 authorized users can access certain resources. Part Two will also cover continuous integration and linking our app with CircleCI, continuous integration, and delivery platform.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.