使用Selenium进行端到端测试
Flask测试客户端不能完全模拟运行中的程序所在的环境,如果依赖运行在客户端浏览器中的JavaScript代码,任何程序都无法正常工作,因为响应发给测试的JavaScript代码无法像在真正的Web浏览器客户端中那样运行
如果测试需要完整的环境,除了使用真正的Web浏览器连接Web服务器中运行的程序外,别无他选,幸运的是,大多数浏览器都支持自动化操作,Selenium是一个Web浏览器自动化工具,支持3种主要操作系统中的大多数Web浏览器,Selenium的Python接口使用pip安装
使用Selenium进行的测试要求程序在Web服务器中运行,监听真实的HTTP请求,让程序运行在后台线程里的开发服务器中,而测试运行在主线程中,在测试的控制下,Selenium启动Web浏览器,并连接程序以执行所需操作
使用这种方法要解决一个问题,即当所有测试都完成后,要停止Flask服务器,而且最好使用一种优雅的方式,以便代码覆盖检测引擎等后台作业能够顺利完成,Werkzeug Web服务器本身就有停止选项,但由于服务器运行在单独的线程中,关闭服务器的唯一方法是发送一个普通的HTTP请求,下例实现了关闭服务器的路由
@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在环境中提供的关闭函数,调用这个函数且请求处理完成后,开发服务器就知道自己需要优雅的退出了
下例是使用Selenium运行测试时测试用例所用的代码结构
# test/test_selenium.py
from selenium import webdriver
class SeleniumTestCase(unittest.TestCase):
client = None
@classmethod
def setUpClass(cls):
# run firefox
try:
cls.client = webdriver.Firefox()
except:
pass
# if not run, pass those test
if cls.client:
#create program
cls.app = create_app('testing')
cls.app_context = cls.app.app_context()
cls.app_context.push()
# ban log, keep simple
import logging
logger = logging.getLogger('werkzeug')
logger.setLevel('ERROR')
# create db, user some generate_fake
db.create_all()
Role.insert_roles()
User.generate_fake(10)
Post.generate_fake(10)
# add manager
admin_role = Role.query.filter_by(permissions=0xff).first()
admin = User(email='john@example.com',
username='john', password='cat',
role=admin_role, confirmed=True)
db.session.add(admin)
db.session.commit()
#in Thread run Flask server
threading.Thread(target=cls.app.run).start()
@classmethod
def tearDownClass(cls):
if cls.client:
# close Flask server and browser
cls.clinet.get('http://localhost:5000/shutdown')
cls.client.close()
# drop db
db.drop_all()
db.session.remove()
# delete program context
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提供的webdriverAPI
启动一个Firefox实例,并创建一个程序和数据库,其中写入了一些供测试使用的初始数据,然后调用标准的app.run()
方法在一个线程中启动程序,完成所有测试后,程序会收到一个发往/shutdown
的请求,进而停止后台线程,随后关闭浏览器,删除测试数据库
Selenium支持Firefox之外的很多Web浏览器,如果想使用其他Web浏览器,查阅官方文档
setUp()
方法在每个测试运行之前执行,如果Selenium无法利用startUpClass()
方法启动Web浏览器就跳过测试,下例是一个使用Selenium进行测试的例子
# test/test_selenium.py
class SeleniumTestCase(unittest.TestCase):
#...
def test_admin_home_page(self):
# into index
self.client.get('http://localhost:5000/')
self.assertTrue(re.search('Hello, \s+Stranger',
self.client.page_source))
# into login page
self.client.find_element_by_link_text('Log In').click()
self.assertTrue('<h1>Login</h1>' in self.client.page_source)
#login
self.client.find_element_by_name('email').\
send_keys('john@example.com')
self.client.find_element_by_name('password').send_keys('cat')
self.client.find_element_by_name('submit').click()
self.assertTrue(re.search('Hello, \s+john!', self.client.page_source))
#into profile
self.client.find_element_by_link_text('profile').click()
self.assertTrue('<h1>john</h1>' in self.clinet.page_source)
这个测试使用setUpClass()
方法中创建的管理员帐户登录程序,然后打开资源页,这里使用的测试方法和使用Flask测试客户端时不一样,使用Selenium进行测试时,测试向Web浏览器发出指令且从不直接和程序交互,发给浏览器的指令和真实用户使用鼠标或键盘执行的操作几乎一样
这个测试首先调用get()
方法访问程序的首页,在浏览器中,这个操作就是在地址栏中输入URL,为了验证这一步操作的结果,测试代码检查页面源码中是否包含"Hello, Stranger!"
这个欢迎消息
为了访问登录页面,测试使用find_element_by_link_text()
方法查找“Log In”
链接,然后再这个链接上调用click()
方法,从而在浏览器中触发一次真正的点击,Selenium提供了很多find_element_by...()
简便方法,可使用不同的方式搜索元素
为了登录程序,测试使用find_element_by_name()
方法通过名字找到表单中的电子邮件和密码字段,然后再使用send_keys()
方法在各字段中填入值,表单的提交通过在提交按钮上调用click()
方法完成,此外, 还要检查针对用户定制的欢迎消息,以确保登录成功且浏览器显示的是首页
测试的最后一部分是找到导航条中的“Profile”
链接,然后点击,为证实资料页已经加载,测试要在页面源码中搜索内容为用户名的标题
测试的必要性
为什么要为了测试而如此折腾Flask测试客户端和Selenium,不管是否喜欢,程序肯定要做测试,如果你自己不做测试,用户就要充当不情愿的测试员,用户发现问题后,就要顶着压力进行修正,检查数据库模型和其他无需在程序上下文中执行的代码很简单,而且有针对性,这类测试一定要做,因为你无需投入过多精力就能保证程序逻辑的核心功能可以正常运行
我们有时候也需要使用Flask测试客户端和Selenium进行端到端形式的测试,不过这类测试编写起来比较复杂,只适用于无法进行单独测试的功能,程序代码应该进行合理组织,尽量把业务逻辑写入数据库模型或独立于程序上下文的辅助类中,这样测试起来才更简单,视图函数中的代码应该保持简洁,仅发挥粘合剂的作用,收到请求后调用其他类中对应的操作或者封装程序逻辑的函数
因此,测试绝对值得,重要的是我们要设计一个高效的测试策略,还要编写能合理利用这一策略的代码