使用Selenium WebDriver测试Flask应用程序-第1部分

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?

是否曾经想过如何为Web应用程序的前端编写测试? 您可能已经具有功能性的后端测试,例如,以确保您的模型和视图正常工作。 但是,您可能不确定如何模拟应用程序的用户进行测试。 您如何测试在浏览器中完成的前端功能,例如注册和登录?

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.

在这个分为两部分的教程中,我将向您展示如何为现有的Python / Flask Web应用程序编写前端测试。 因此,您应该已经具有一个正常运行的应用程序,以及一个已安装必要软件依赖项的虚拟环境。 我们将使用Project Dream Team,这是我在一个分为三部分的教程(这里是第一部分第二部分第三部分 )中构建的CRUD Web应用程序。 我建议您阅读本教程并构建应用程序,特别是如果您不熟悉Flask或在Flask中进行测试。 如果您之前已经构建并测试过Flask应用程序,或者不想阅读本教程,则可以从GitHub存储库中克隆完整的应用程序, 在此处并按照README文件中的设置说明进行操作。

测试概述 ( 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.

回顾一下,我们将要测试的应用程序是员工管理应用程序。 该应用程序允许用户注册为员工并登录。 它还允许管理员用户查看所有员工,以及创建部门和角色,然后将其分配给每个员工。

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.

应用程序教程中已经编写了后端测试,用于测试应用程序的模型和视图是否按预期工作。 我们将在本教程中编写的前端测试将使用浏览器中的应用程序模拟用户。 对于这些测试,我们将创建测试数据,然后测试该应用程序的注册,登录和CRUD功能。

测试目录结构 ( 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.py 。 与其将所有测试放在一个文件中,不如创建一个tests目录。 在其中创建两个新文件: __init__.py test_front_end.pytest_front_end.py 。 将tests.py文件移动到tests目录,并将其重命名为test_back_end.py 。 现在,您的tests目录应如下所示:

└── 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:

__init__.py文件将tests目录初始化为Python包。 test-back_end.py将包含我们所有的后端测试,而test_front_end.py将包含我们所有的前端测试。 如下更新test_back_end.py文件:

# 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.

因为我们现在有多个测试文件,所以我们需要一种可以一次运行所有测试的方法。 必须单独运行每个测试文件会很不方便。 为此,我们将使用鼻子2软件包,该软件包扩展了Python的单元测试框架unittest ,并使测试更加容易。

$ pipinstall 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:

现在,我们将使用nose2命令运行测试。 该命令查找名称以“ test”开头的所有文件,并在名称以“ test”开头的文件中运行方法。 这意味着,如果您错误地命名测试文件和方法,它们将不会由nas2运行。 首先,请记住确保您MySQL服务器正在运行。 您可以通过运行以下命令来执行此操作:

$ mysqld

Then, in another terminal window, run the tests:

然后,在另一个终端窗口中,运行测试:

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

OK

Selenium WebDriver简介 ( 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.

Selenium是用于自动实现各种目的的Web浏览器的工具套件。 当编写基于浏览器的测试时, Selenium WebDriver特别有用。

Begin by installing Selenium:

首先安装Selenium:

$ pipinstall selenium

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.

现在,让我们设置Selenium。 我们正在使用Flask Testing ,这是为Flask提供单元测试实用程序的扩展。 要将Selenium与Flask Testing一起使用,我们需要从LiveServerTestCase类继承,这将使我们能够在测试期间运行实时服务器。

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.

我们将使用ChromeDriver (Chrome的WebDriver)。 这将使我们能够在Chrome浏览器上运行前端测试。 如果您使用的是MacOS,则只需使用brew install chromedriver命令即可安装它。 对于其他平台,请按照此入门指南下载并正确设置ChromeDriver。 请注意,您将需要安装Chrome浏览器。

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

如果您希望使用Firefox浏览器进行测试,则可以安装并设置FirefoxDriver

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/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.

接下来,我们创建一个TestBase类,后续的测试类将继承该类。 在其中,我们具有create_app方法,Flask Testing要求该方法返回一个应用程序实例。 在此方法中,我们指定Flask配置和测试数据库,就像在后端测试中所做的一样。 我们还指定运行实时服务器的端口。 Flask默认使用端口5000。 为实时服务器更改此设置将确保测试在另一个端口(在本例中为8943端口)上运行,从而防止与可能正在运行的另一个Flask应用程序发生任何冲突。

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.

setUp方法中,我们将测试驱动程序设置为ChromeDriver。 (例如,如果使用其他webdriver,例如Firefox,请使用self.driver = webdriver.Firefox() 。)注意self.get_server_url方法; 我们用它来获得家庭URL。 我们还使用最初创建的变量来创建测试员工,测试管理员用户,测试部门和测试角色。 在tearDown方法中,我们退出Web驱动程序,并清除测试数据库。 在每次测试之前都会调用setUp方法,而在每次测试之后都会调用tearDown方法。 在运行测试之前拥有测试数据非常重要,因为我们的大多数测试都将需要现有的用户,部门和角色。 为每个测试创建新的用户,部门和角色将是重复的,因此在setUp方法中进行此操作更简单且更setUp

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.

在基类中,我们有一个测试方法test_server_is_up_and_running 。 此方法仅确保测试服务器按预期工作,并返回200 OK状态代码。

Run the tests using the nose2 command:

使用nose2命令运行测试:

$ 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.

它表明它运行了15个测试,其中包括我们最初拥有的14个后端测试以及1个前端测试。

寻找元素 ( 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.

到目前为止, test_front_end.py文件中只有一个测试。 我们需要编写其他测试来模拟实际使用该应用程序的用户,例如单击链接和按钮,将数据输入表单等。 要告诉Selenium单击特定按钮或在表单的特定字段中输入数据,我们需要为这些Web元素使用唯一的标识符。

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.

Selenium提供了一些方法,我们可以通过各种方法来调用这些方法来查找Web元素,例如通过id ,通过名称 ,通过XPath和通过CSS选择器 。 我发现按id定位元素是最简单的方法之一。 它只需要您为需要定位的元素赋予id属性。

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:

我们将从为应用程序中的菜单链接分配id属性开始,以便我们可以指导测试用户根据需要单击它们。 打开templates/base.html文件,并更新具有id属性的菜单项,如下所示:

<!-- 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.

我们将需要定位的大多数其他Web元素已经具有id属性。 对于那些没有的人,我们将通过它们CSS类名称和选择器找到它们。

测试身份验证蓝图 ( 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.

我们将从为Auth蓝图编写测试开始,该蓝图处理注册,登录和注销。 这些测试需要处理极端情况,例如用户输入无效数据,以便我们确保应用正确响应。

注册测试 (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:

我们将从编写注册测试开始。 在TestBase类之后,将以下代码添加到test_front_end.py文件中:

# 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.

我们创建了一个类TestRegistration ,该类继承自TestBase类,并且具有三个测试方法。 请注意,文件中的if __name__ == '__main__':部分应始终位于文件的底部,因此所有新添加的代码都将位于该文件的底部。

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.

如果用户正确填写了注册表单中的所有字段,则test_registration方法测试用户是否可以成功创建帐户。 它还测试用户注册后是否将用户重定向到登录页面,并显示一条消息邀请他们登录。 test_registration_invalid_email方法测试在电子邮件字段中输入无效的电子邮件格式将阻止提交注册表,并显示适当的错误消息。 test_registration_confirm_password方法测试“密码”和“确认密码”字段中的数据必须匹配,如果不匹配,则会显示相应的错误消息。

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.

请注意send_keys方法(用于在表单字段中输入数据)和click方法(用于单击按钮和链接等Web元素)。 此外,请注意time.sleep方法。 我们使用它来暂停测试,以便浏览器有时间完全加载所有Web元素,然后再继续下一步。

登录测试 (Login Tests)

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

下一个测试是用于登录的。添加TestLogin类,如下所示:

# 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.

test_login方法测试是否为注册用户输入了正确的电子邮件和密码组合,然后将该用户重定向到显示其用户名的仪表板。 它与test_admin_login方法相同,除了后者测试重定向到管理仪表板的管理员用户的登录名。 test_invalid_email_format测试用户是否在电子邮件字段中输入非电子邮件,则不能提交登录表单。 test_login_wrong_emailtest_login_wrong_password方法测试用户无法使用无效的凭据登录,并显示相应的错误消息。

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.

接下来,我们将为Admin蓝图编写测试,这是应用程序的CRUD功能所在的位置。 在这些测试中,我们将模拟管理员用户创建,查看,更新和删除部门和角色。 我们还将模拟一个为用户分配部门和角色的管理员用户。

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:

在您的front_end_tests.py文件中,在测试变量之后和TestRegistration类之前添加一个CreateObjects类:

# 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:

接下来,我们将编写测试。 添加TestDepartments类后TestLogin类,如下所示:

# 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.

test_add_department方法测试管理员用户可以使用添加部门表单来添加部门,而test_add_existing_department方法测试他们不能添加已经存在的部门名称。 test_edit_department方法测试可以使用编辑表单来编辑部门。 注意clear方法,该方法使我们可以在输入其他文本之前清除所有现有文本的文本框。 最后,可以使用“部门”页面中的“删除”链接来删除一个部门的test_delete_department测试。

角色测试 (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:

您可能还需要编辑add_departmentadd_role视图,以处理在尝试添加已存在的部门或角色时在事务回滚期间可能发生SQLAlchemy异常。 要处理此异常,请在add_departmentadd_role视图中编辑try-except块,如下所示:

# 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.

使用雇员页面中的分配链接, test_assign方法将现有部门和角色分配给现有用户。 test_reassign方法将新部门和角色添加到数据库,然后将它们分配给现有员工。 请注意我们从Selenium导入的Select类。 从中,我们使用select_by_visible_text方法从下拉菜单中选择部门和角色。

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.

就是第一部分了! 回顾一下,在本教程中,您了解了Selenium WebDriver,以及如何通过模拟应用程序的用户将其用于运行前端测试。 您还了解了Web元素以及如何使用Selenium的某些内置方法找到它们。 我们已经针对部门和角色进行了注册,登录和执行CRUD操作的书面测试。

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.

在第二部分中,我们将编写权限测试,以确保只有授权用户才能访问某些资源。 第二部分还将介绍持续集成以及将我们的应用程序与CircleCI (一个持续集成和交付平台)链接。

翻译自: https://scotch.io/tutorials/test-a-flask-app-with-selenium-webdriver-part-1

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值