Top shelf web developer training.

Guided Paths

Follow our crafted routes to reach your goals.

Video Courses

Premium videos to build real apps.

Written Tutorials

Code to follow and learn from.

Find Your
Opportunity HIRED
Dismiss
Up

Test a Flask App with Selenium WebDriver – Part 2

Related Course

Get Started with JavaScript for Web Development

JavaScript is the language on fire. Build an app for any platform you want including website, server, mobile, and desktop.

This is the second and final part of a tutorial on how to test a Python/Flask web app with Selenium webdriver. We are testing Project Dream Team, an existing CRUD web app. Part One introduced Selenium WebDriver as a web browser automation tool for browser-based tests. By the end of Part One, we had written tests for registration, login, performing CRUD operations on departments and roles, as well as assigning departments and roles to employees.

In Part Two, we will write tests to ensure that protected pages can only be accessed by authorised users. We will also integrate our app with CircleCI, a continuous integration and delivery platform. I have included a demo video showing all the tests running, so be sure to check it out!

Permissions Tests

Recall that in the Dream Team app, there are two kinds of users: regular users, who can only register and login as employees, and admin users, who can access departments and roles and assign them to employees. Non-admin users should not be able to access the departments, roles, and employees pages. We will therefore write tests to ensure that this is the case.

In your tests/test_front_end.py file, add the following code:

# tests/test_front_end.py

class TestPermissions(CreateObjects, TestBase):

    def test_permissions_admin_dashboard(self):
        """
        Test that non-admin users cannot access the admin dashboard
        """
        # Login as non-admin user
        self.login_test_user()

        # Navigate to admin dashboard
        target_url = self.get_server_url() + url_for('home.admin_dashboard')
        self.driver.get(target_url)

        # Assert 403 error page is shown
        error_title = self.driver.find_element_by_css_selector("h1").text
        self.assertEqual("403 Error", error_title)
        error_text = self.driver.find_element_by_css_selector("h3").text
        assert "You do not have sufficient permissions" in error_text

    def test_permissions_list_departments_page(self):
        """
        Test that non-admin users cannot access the list departments page
        """
        # Login as non-admin user
        self.login_test_user()

        # Navigate to admin dashboard
        target_url = self.get_server_url() + url_for('admin.list_departments')
        self.driver.get(target_url)

        # Assert 403 error page is shown
        error_title = self.driver.find_element_by_css_selector("h1").text
        self.assertEqual("403 Error", error_title)
        error_text = self.driver.find_element_by_css_selector("h3").text
        assert "You do not have sufficient permissions" in error_text

    def test_permissions_add_department_page(self):
        """
        Test that non-admin users cannot access the add department page
        """
        # Login as non-admin user
        self.login_test_user()

        # Navigate to admin dashboard
        target_url = self.get_server_url() + url_for('admin.add_department')
        self.driver.get(target_url)

        # Assert 403 error page is shown
        error_title = self.driver.find_element_by_css_selector("h1").text
        self.assertEqual("403 Error", error_title)
        error_text = self.driver.find_element_by_css_selector("h3").text
        assert "You do not have sufficient permissions" in error_text

    def test_permissions_list_roles_page(self):
        """
        Test that non-admin users cannot access the list roles page
        """
        # Login as non-admin user
        self.login_test_user()

        # Navigate to admin dashboard
        target_url = self.get_server_url() + url_for('admin.list_roles')
        self.driver.get(target_url)

        # Assert 403 error page is shown
        error_title = self.driver.find_element_by_css_selector("h1").text
        self.assertEqual("403 Error", error_title)
        error_text = self.driver.find_element_by_css_selector("h3").text
        assert "You do not have sufficient permissions" in error_text

    def test_permissions_add_role_page(self):
        """
        Test that non-admin users cannot access the add role page
        """
        # Login as non-admin user
        self.login_test_user()

        # Navigate to admin dashboard
        target_url = self.get_server_url() + url_for('admin.add_role')
        self.driver.get(target_url)

        # Assert 403 error page is shown
        error_title = self.driver.find_element_by_css_selector("h1").text
        self.assertEqual("403 Error", error_title)
        error_text = self.driver.find_element_by_css_selector("h3").text
        assert "You do not have sufficient permissions" in error_text

    def test_permissions_list_employees_page(self):
        """
        Test that non-admin users cannot access the list employees page
        """
        # Login as non-admin user
        self.login_test_user()

        # Navigate to admin dashboard
        target_url = self.get_server_url() + url_for('admin.list_employees')
        self.driver.get(target_url)

        # Assert 403 error page is shown
        error_title = self.driver.find_element_by_css_selector("h1").text
        self.assertEqual("403 Error", error_title)
        error_text = self.driver.find_element_by_css_selector("h3").text
        assert "You do not have sufficient permissions" in error_text

    def test_permissions_assign_employee_page(self):
        """
        Test that non-admin users cannot access the assign employee page
        """
        # Login as non-admin user
        self.login_test_user()

        # Navigate to admin dashboard
        target_url = self.get_server_url() + url_for('admin.assign_employee', id=1)
        self.driver.get(target_url)

        # Assert 403 error page is shown
        error_title = self.driver.find_element_by_css_selector("h1").text
        self.assertEqual("403 Error", error_title)
        error_text = self.driver.find_element_by_css_selector("h3").text
        assert "You do not have sufficient permissions" in error_text

We begin by creating a class TestPermissions, which inherits from the CreateObjects and TestBase classes that we wrote in Part One. In each of the test methods inside the class, we login as a non-admin user, and then attempt to access a protected page. First, we test the departments pages (list and add), then the roles pages (list and add), and finally the employees pages (list and assign). In each method, we test that the 403 error page is shown by asserting that the appropriate page title ("403 Error") and text ("You do not have sufficient permissions to access this page") are shown on the page.

Take note of the difference between the assertEqual method and the assert ... in statement. The former checks that two things are exactly the same, whereas the latter checks that the first thing is contained in the second. In the case of our tests, "403 Error" and the error page title are exactly the same, so we can use assertEqual. For the second assertion, we are merely checking that the words "You do not have sufficient permissions" are contained in the error page text. The assert ... in statement is ideal when you don't want to check for identicalness, but rather that a certain important word or phrase is contained in the element in question.

Let's run our tests now:

$ nose2
......................................
----------------------------------------------------------------------
Ran 38 tests in 168.981s

OK

Continuous Integration and Continuous Delivery

You may have heard of continuous integration (CI), but you may not be very clear on what exactly it is or how to implement it in your development workflow. Well, CI refers to a software development pratice of integrating project code into a shared repository frequently, typically multiple times a day. CI usually goes hand-in-hand with automated builds and automated testing, such that each time code is pushed into the shared repo, the code is run and tested automatically to ensure it has no errors.

The idea is that small changes in the code are integrated to the main repo frequently, which makes it easier to catch errors should they occur and troubleshoot them. This is in contrast to a scenario where integration is done less often and with more code, making it more difficult to detect which change was responsible if an error was to occur.

Martin Fowler, Chief Scientist at ThoughtWorks, put it well when he said:

Continuous Integration doesn’t get rid of bugs, but it does make them dramatically easier to find and remove.

Continuous delivery entails building and handling your code in such a way that it can be released into production at any time. Practising continuous delivery means not having any code in your main repo that you wouldn't want to deploy. Sometimes, this even means that any code that is pushed to the main repo is automatically put in production if the build is successful and all tests pass. This is called continuous deployment.

Introducing CircleCI

Now that you're up to speed with continuous integration and continuous delivery, let's get familiar with one of the most popular continuous integration and delivery platforms today: CircleCI. CircleCI is quick and easy to set up. It automates software builds and testing, and also supports pushing code to many popular hosts such as Heroku and Google Cloud Platform.

To start using CircleCI, sign up by authenticating your GitHub or Bitbucket account. Once you login, navigate to the Projects page where you can add your project repository. Select Build Project next to your repository name, and CircleCI will start the build.

Uh oh! The first build fails. You'll notice the disconcerting red colour all over the page, the multiple error messages, and even the disheartening red favicon in your browser, all of which denote failure. First of all, congratulations on your first failed build! :) Secondly, don't worry; we haven't configured CircleCI or our app yet, so it's no wonder the build failed! Let's get to work setting things up to turn the red to green.

Environment Variables

We'll start by adding some important environment variables to CircleCI. Because we won't be reading from the instance/config.py file, we'll need to add those variables to CircleCI. On the top right of the build page on CircleCI, click the cog icon to access the Project Settings. In the menu on the left under Build Settings, click on Environment Variables. You can now go ahead and add the following variables:

  1. SECRET_KEY. You can copy this from your instance/config.py file.

  1. SQLALCHEMY_DATABASE_URI. We will use CircleCI's default circle_test database and ubuntu user, so our SQLALCHEMY_DATABASE_URI will be mysql://ubuntu@localhost/circle_test.

You should now have all two environment variables:

The circle.yml File

Next, create a circle.yml file in your root folder and in it, add the following:

machine:
  python:
    version: 2.7.10
test:
  override:
    - nose2 

We begin by indicating the Python version for our project, 2.7.10. We then tell CircleCI to run our tests using the nose2 command. Note that we don't need to explicitly tell CircleCI to install the software dependencies because it automatically detects the requirements.txt file in Python projects and installs the requirements.

The create_app Method

Next, edit the create_app method in the app/__init__.py file as follows:

# app/__init__.py

def create_app(config_name):
    # modify the if statement to include the CIRCLECI environment variable
    if os.getenv('FLASK_CONFIG') == "production":
        app = Flask(__name__)
        app.config.update(
            SECRET_KEY=os.getenv('SECRET_KEY'),
            SQLALCHEMY_DATABASE_URI=os.getenv('SQLALCHEMY_DATABASE_URI')
        )
    elif os.getenv('CIRCLECI'):
        app = Flask(__name__)
        app.config.update(
            SECRET_KEY=os.getenv('SECRET_KEY')
        )
    else:
        app = Flask(__name__, instance_relative_config=True)
        app.config.from_object(app_config[config_name])
        app.config.from_pyfile('config.py')

This checks for CircleCI's built-in CIRCLECI environment variable, which returns True when on CircleCI. This way, when running the tests on CircleCI, Flask will not load from the instance/config.py file, and will instead get the value of the SECRET_KEY configuration variable from the environment variable we set earlier.

The Test Files

Now edit the create_app method in the tests/test_front_end.py file as follows:

# tests/test_front_end.py

# update imports
import os

class TestBase(LiveServerTestCase):

    def create_app(self):
        config_name = 'testing'
        app = create_app(config_name)
        if os.getenv('CIRCLECI'):
            database_uri = os.getenv('SQLALCHEMY_DATABASE_URI')
        else:
            database_uri = 'mysql://dt_admin:dt2016@localhost/dreamteam_test',
        app.config.update(
            # Specify the test database
            SQLALCHEMY_DATABASE_URI=database_uri,
            # Change the port that the liveserver listens on
            LIVESERVER_PORT=8943
        )
        return app

This ensures that when the tests are running on CircleCI, Flask will get the SQLALCHEMY_DATABASE_URI from the environment variable we set earlier rather than using the test database we have locally.

Finally, do the same for the create_app method in the tests/test_back_end.py file:

# tests/test_back_end.py

# update imports
import os

class TestBase(TestCase):

    def create_app(self):
        config_name = 'testing'
        app = create_app(config_name)
        if os.getenv('CIRCLECI'):
            database_uri = os.getenv('SQLALCHEMY_DATABASE_URI')
        else:
            database_uri = 'mysql://dt_admin:dt2016@localhost/dreamteam_test',
        app.config.update(
            # Specify the test database
            SQLALCHEMY_DATABASE_URI=database_uri
        )
        return app

Push your changes to your repository. You'll notice that as soon as you push your code, CircleCI will automatically rebuild the project. It'll take a few minutes, but the build should be successful this time. Good job!

Status Badge

CircleCI porvides a status badge for use on your project repository or website to display your build status. To get your badge, click on the Status Badges link in the menu on the left under Notifications. You can get the status badge in a variety of formats, including image and MarkDown.

Conclusion

You are now able to write a variety of front-end tests for a Flask application with Selenium WebDriver. You also have a good understanding of continuous integration and continuous delivery, and can set up a project on CircleCI. I hope you've enjoyed this tutorial! I look forwrad to hearing your feedback and experiences in the comment section below.

For more information on continuous integration in Python with CircleCI, you may refer to this Scotch tutorial by Elizabeth Mabishi.

Mbithe Nzomo

I am a software developer at Andela. 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 dev. I am passionate about travelling, good music, and increasing diversity in the tech industry.