第十五章 测试(三)

使用Selenium进行端到端测试

       Flask中的测试客户端不能完全模拟运行中的应用所处的环境。例如:如果应用依赖在客户端浏览器中运行的JS代码的话,就不能使用Flask测试客户端,因为返回给测试的响应中的JS代码不会执行。

       多数Web浏览器都支持自动化操作。Selenium是一个Web浏览器自动化工具,支持3中主要操作系统中的多数主流Web浏览器。使用pipenv安装selenium的python接口:

pipenv install selenium --dev

除了selenium本身,还需要安装浏览器即对应版本的驱动。几个主流浏览器的Web驱动的下载地址如下:

Chrome https://sites.google.com/a/chromium.org/chromedriver/downloads
Firefox https://github.com/mozilla/geckodriver/releases
Safari https://webkit.org/blog/6900/webdriver-support-in-safari-10/

       下载驱动后我们要把驱动程序的路径添加到系统PATH环境变量中。这样Selenium才能执行对应的程序。最简单的方式是把驱动程序放到Python解析器所在的目录下,如:

       使用selenium进行测试,要求应用在Web服务器中运行,监听真实的HTTP请求。本节使用的方法是,让应用运行在后台线程里的开发服务器中,而测试运行在主线程中。在测试的控制下,Selenium启动web浏览器,连接应用,执行所需操作。使用这种方法需要解决一个问题,即所有测试完成后,要停止Flask服务器,而且最好使用一种优雅的方式,以便代码覆盖度检测引擎等后台作业能够顺利完成。

       Werkzeug Web服务器本身就有停止选项,但由于服务器运行在单独线程中,关闭服务器唯一的方法是发送一个普通的HTTP请求。

app/main/views.py 关闭服务器的路由

@main.route('/shutdown')
def server_shutdown():
    if not current_app.testing:
        abort(404)
    shutdown = request.environ.get('werkzeug.server.shutdown')
    if not shutdown:
        abort(500)
    shutdown()
    return 'Shutting down...'

       仅应用运行在测试环境中时,关闭服务器的路由才可用。调用werkzeug对环境开放的关闭函数。调用这个函数且处理完请求之后,开发服务器就知道自己需要优雅地退出了。

tests/test_selenium.py:使用Selenium运行测试的框架

class SeleniumTestCase(unittest.TestCase):
    client = None

    @classmethod
    def setUpClass(cls):
        # 启动Chrome
        # 不加载图片和CSS,加快测试速度
        prefs = {'profile.managed_default_content_settings.images':2, 'permissions.default.stylesheet':2}
        options = webdriver.ChromeOptions()
        # options.add_argument('headless')
        options.add_experimental_option('prefs', prefs)
        try:
            cls.client = webdriver.Chrome(options=options)
        except:
            pass

        # 如果无法启动浏览器,跳过这些测试
        if cls.client:
            # 创建应用
            cls.app = create_app('testing')
            cls.app_context = cls.app.app_context()
            cls.app_context.push()

            # 禁止日志,保持输出简洁
            import logging
            logger = logging.getLogger('werkzeug')
            logger.setLevel("ERROR")

            # 创建数据库,并使用一些虚拟数据填充
            db.create_all()
            Role.insert_rows()
            fake.users(10)
            fake.posts(10)

            # 添加管理员
            admin_role = Role.query.filter_by(name='Administrator').first()
            admin = User(email='[email protected]', username='admin', password='admin', role=admin_role, confirmed=True)
            db.session.add(admin)
            db.session.commit()

            # 在一个线程中启动Flask服务器
            cls.server_thread = threading.Thread(target=cls.app.run, kwargs={'debug': False})
            cls.server_thread.start()
            # 确保服务器已完成启动
            time.sleep(2)

    @classmethod
    def tearDownClass(cls):
        if cls.client:
            # 关闭Flask服务器和浏览器
            cls.client.get('http://localhost:5000/shutdown')
            cls.client.quit()
            cls.server_thread.join()

            # 销毁数据库
            db.drop_all()
            db.session.remove()

            # 删除应用上下文
            cls.app_context.pop()

    def setUp(self):
        if not self.client:
            self.skipTest('Web browser not available')

    def tearDown(self):
        pass

       setUpClass()和tearDownClass()类方法分别在这个类中的全部测试运行之前和之后执行。setUpClass()方法使用Selenium提供的webdriver API启动一个Chrome实例,然后创建一个应用和数据库,在其中写入写入一些供测试使用的初始数据。然后调用app.run()方法在一个线程中启动应用。完成所有测试后,应用会收到一个发往/shutdown的请求,使后台线程终止。随后,关闭浏览器,删除测试数据库。

       setUp()方法在每个测试运行之前执行,如果Selenium无法利用startUpClass()方法启动Web浏览器就跳过测试。

tests/test_selenium.py Selenium单元测试用例

    def test_admin_home_page(self):
        # 进入首页
        self.client.get('http://localhost:5000/')
        self.assertTrue(re.search('Hello,\s*Stranger!', self.client.page_source))

        # 进入登录页面
        self.client.find_element_by_link_text('Log In').click()
        self.assertIn('<h1>Login</h1>', self.client.page_source)
        time.sleep(2)

        # 登录
        self.client.find_element_by_name('email').send_keys('[email protected]')
        self.client.find_element_by_name('password').send_keys('admin')
        self.client.find_element_by_name('submit').click()
        time.sleep(2)
        self.assertTrue(re.search('Hello,\s*admin', self.client.page_source))

        # 进入用户资料页面
        self.client.find_element_by_link_text('Profile').click()
        self.assertIn('<h1>admin</h1>', self.client.page_source)

       使用Selenium进行测试时,测试向Web浏览器发出指令,从不直接与应用交互。发给浏览器的指令与真实用户使用鼠标或键盘执行的操作几乎一样。为了访问登录页面,测试使用find_element_by_link_text()方法查找"Log In"链接,然后在这个链接上调用click方法,从而在浏览器中触发一次点击,Selenium提供了很多find_element_by_...的方法通过名称找到表单中的电子邮件和密码字段,然后再调用send_keys()方法在各字段中填入值。

使用Selenium时,请注意以下几点:

  1.  
    options = webdriver.ChromeOptions()
    options.add_argument('headless')
    cls.client = webdriver.Chrome(options=options)

    Chrome以无头浏览器的形式运行,我们看不到任何弹出的窗口,注释掉options.add_argument('headless'),即可启动带窗口的常规Chrome实例。

  2. prefs = {
        'profile.managed_default_content_settings.images':2,
        'permissions.default.stylesheet':2
    }
    options = webdriver.ChromeOptions()
    options.add_experimental_option('prefs', prefs)
    cls.client = webdriver.Chrome(options=options)

    由于我们的应用中,部分界面涉及加载大量的头像资源,页面加载速度非常缓慢。设置不加载CSS资源以及图片资源,可以大幅度提升速度。

  3. 元素遮挡问题:和我们手动操作相同,如果页面上的某个元素被另一个元素遮挡了,那么我们无法使用Selenium单击它。

  4.  元素引用失效:当我们把指向某个元素的引用保存在python变量中时,这个变量仅仅在当前页面可用。如果你这时跳转到新的页面,那么指向旧页面的引用也会随之失效;

  5. 页面加载时间:和手动操作类似,页面加载需要时间。如果某个操作需要耗费较长的时间,那么你同时需要使用time.sleep()来休眠程序进行等待,否则相应的操作可能无法执行。

值得测试吗?

       不管你喜不喜欢,应用肯定要做测试。如果自己不做,用户就要充当被动的测试员,用户发现问题后,就要顶着压力修改。检查数据库模型和其它无需在应用上下文中执行的代码很简单,而且有针对性,这类测试一定要做。

       我们有时候也需要使用Flask测试客户端和Selenium进行端到端形式的测试,不过这类测试编写起来比较复杂,只适用于无法单独测试的功能。应该合理组织应用代码,尽量把业务逻辑写入独立于应用上下文的模块中,这样测试起来才更简单。视图函数中的代码应该保持简洁,仅发挥黏合剂的作用,收到请求后调用其它类中相应的操作或者封装应用逻辑的函数。

       下一小结,我们将使用coverage工具,计算测试覆盖率及详细的覆盖情况。

发布了132 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geroge_lmx/article/details/104847607