理论上一个测试只应该测试一件事,所以没必要在每隔功能测试中都测试登录和退出功能。所以需要找到一种方法“作弊”,跳过认证,这样就不用花时间等待执行完重复的测试路径了。
在功能测试中去除重复时不要做得太过火了。功能测试的优势之一是,可以捕获应用不同部分之间交互时产生的神秘莫测的表现。
17.1 实现创建好会话,跳过登陆过程
用户再次访问网站时 cookie 仍然存在,这种现象很常见。所以“作弊”手段具体做法如下:
# functional_tests/test_my_lists.py
from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model
from django.contrib.sessions.backends.db import SessionStore
from .base import FunctionalTest
User = get_user_model()
class MyListsTest(FunctionalTest):
def create_pre_authenticated_session(self, email):
user = User.objects.create(email=email)
session = SessionStore()
session[SESSION_KEY] = user.pk # 在数据库中创建一个会话对象。会话键的值是用户对象的主键,即用户的电子邮件地址
session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]
session.save()
## 为了设定 cookie,我们要先访问网站
## 而 404 页面是加载最快的
self.browser.get(self.server_url + "/404_no_such_url/")
self.browser.add_cookie(dict(
name=settings.SESSION_COOKIE_NAME,
value=session.session_key, # 然后把一个 cookie 添加到浏览器中,cookie 的值和服务器中的会话匹配。这样再次访问网站时,服务器就能识别已登录的用户。
path="/",
))
注意,这种做法仅当使用 LiveServerTestCase 时才有效,所以已创建的 User 和 Session 对象只存在于测试服务器的数据库中。等下会修改实现的方式,让这个测试也能在过渡服务器里的数据库中运行。
JSON 格式的测试固件有危害
使用测试数据预先填充数据库的过程,例如存储 User 对象及其相关的 Session 对象,叫做设定"测试固件"(test fixture)。
Django 原生支持把数据库中的数据保存为 JSON 格式(使用 manage.py dumpdata 命令)。如果在 TestCase 中使用类属性 fixtures,运行测试时 Django 会自动加载 JSON 格式的数据。
越来越多的人建议不要使用 JSON 格式的固件。如果修改了模型,这种固件维护起来像噩梦。因此,只要可以,就直接使用 Django ORM 加载数据,或者使用 factory_boy 之类的工具。
要检查这种做法是否可行,最好使用前面测试中定义的 wait_to_be_logged_in 函数。要想在不同的测试中访问这个方法,就要把它连同另外几个方法一起移到 FunctionalTest 类中。
相应调整一下 test_login.py :
为了确认我们没有破坏现有功能,再次运行登录测试:
现在可以为 “My Lists” 页面编写一个占位测试,检查实现创建认证会话的做法是否可行:
测试结果为 OK,现在可以提交了:
17.2 实践时检验真理的唯一标准:在过渡服务器中捕获最后的问题
这个功能测试在本地运行一切正常,但在过渡服务器中遇到了意料之外的问题,为了解决这个问题,要找到在测试服务器中管理数据库的方法:
然后重启 Gunicron
接着运行功能测试:
无法登陆,不管真的是用 Persona 还是已经使用通过认证的会话都不行。这说明测试有问题。我们需要练习如何使用服务器端调试技术。
为了记录这个问题,配置 Gunicorn,让它记录日志。在服务器中使用 vi 或 nano 按照下面的方式调整 Gunicorn 的配置:
这样配置之后,Gunicorn 会在 ~/sites/$SITENAME 文件夹中保存访问日志和错误日志。然后在 authenticate 函数中调用日志相关的函数输出一些调试信息。
像这样直接调用日志记录器(logging.warning )不太好
还要确保 settings.py 中仍有 LOGGING 设置,这样调试信息才能输送到终端。
再次重启 Gunicron,然后运行功能测试。在这些操作执行的过程中,可以使用下面的命令监视日志:
注意 Persona 系统的一个重要部分,即认证只在特定的域名中有效。在 accounts/authentication.py 中把域名硬编码即可。
按照上面那样打印不出来日志,原因未知,自己重新这样设定就可以了:
17.2.2 修正 Persona 引起的这个问题
修正方法如下所示,在本地电脑中修改代码,首先把 DOMOAIN 变量移到 settings.py中,稍后可以在部署脚本中重定义这个变量:
然后修改测试,应对上述改动:
接着,修改实现方式:
运行测试确认一下:
然后再修改 fabfile.py,让它调整 settigns.py 中的域名,同时删除使用 sed 修改 ALLOWED_HOSTS 那两行多余的代码:
重新部署,看输出中有没执行 sed 修改 DOMAIN 的值。
17.3 在过渡服务器中管理测试数据库
现在可以再次运行功能测试,此时又会看到一个失败测试,因为无法创建已经通过认证的会话,所以针对 “My Lists` 页面的测试失败了:
失败的真正原因是 create_pre_authenticated_session 函数只能操作本地数据库。要找到一种方法,管理服务器中的数据库。
17.3.1 创建会话的 Django 管理命令
若想在服务器中操作,就要编写一个自成一体的脚本,在服务器中的命令行里执行。大多数情况下都会使用 Fabric 执行这样的脚本。
尝试编写可在 Django 环境中运行的独立脚本(和数据库交互等)。有些问题需要谨慎处理,例如正确设定 DJANGO_SETTINGS_MODULE 环境变量,还要正确处理 sys.path。与其在这些细节上浪费时间,其实 Django 允许我们自己创建”管理命令“(可以使用 python manage.py 运行的命令),可以把一切琐碎的事情都交给 Django 完成。管理命令保存在应用的 management/command 文件夹中:
管理命令的样板代码是一个类,继承自 django.core.management.BaseCommand,而且定义了一个名为 handle 的方法:
create_pre_authenticated_session 函数的代码从 test_my_lists.py 文件中提取而来。handle 方法从命令行的第一个参数中获取电子邮件地址,返回一个将要存入浏览器 cookie 中的会话键。这个管理命令还会把会话期间打印到命令行中,试一下这个命令:
还要做一步设置——把 functional_tests 加入 settings.py,让 Django 把它识别为一个可能包含管理命令和测试的真正应用。
现在这个管理命令可以使用了:
17.3.2 让功能测试在服务器上运行管理命令
接下来调整 test_my_lists.py 文件中的测试,让它在本地服务器中运行本地函数,但是在过渡服务器中运行管理命令:
看一下如何判断是否运行在过渡服务器中。self.against_staging 的值在 base.py 中设定:
17.3.3 使用 subprocess 模块完成额外的工作
我们的测试使用 Python 3,不能直接调用 Fabric 函数,因为 Fabric 只能在 Python 2 中使用。所以要做些额外工作,像部署服务器时一样,在新进程中执行 fab 命令。要做的额外工作如下,代码写入 server_tools 模块中:
这里使用 subprocess 模块通过 fab 命令调用几个 Fabric 函数。
如果使用自定义的用户名或密码,需要修改调用 subprocess 那行代码,和运行自动化部署脚本时 fab 命令的参数保持一致。
最后,看一下 fabfile.py 中定义的那两个在服务器端运行的命令。这两个命令的作用是还原数据库和设置会话:
首先,在本地运行测试,确认没有造成任何破坏:python3 manage.py test functional_tests.test_my_lists。
然后,在服务器中运行。先把代码推送到服务器中:
再运行测试。注意,现在指定 liveserver 参数的值时可以包含 elspeth@:
之后还可以运行全部测试确认一下。
作者展示了管理测试数据库的一种方法,也可以试验其他方式。例如,使用 MySQL 或 Postgres,可以打开一个 SSH 隧道连接服务器,使用端口转发直接操作数据库。然后修改 settings.DATABSES,让功能测试使用隧道连接的端口和数据库交互。
警告:小心,不要在线上服务器中运行测试
我们现在编写的代码能直接影响服务器中的数据库。一定要非常小心,别在错误的主机中运行功能测试,把生产数据库清空了。
此时,可以考虑使用一些安全防护措施。例如,把过渡环境和生产环境放在不同的服务器中,而且不同的服务器使用不同的口令认证密钥对。
在生产环境的数据副本中运行测试也有同样的危险。
接下来要把日志相关的代码集成到应用中。把输出日志的代码放在那儿,并且纳入版本控制,有助于调试以后遇到的登陆问题。
先把 Gunicorn 的配置保存到 deploy_tools 文件夹里的临时文件中。
前面调用 logging.warning 时,使用的是根日志记录器。一般来说,这么做并不好,因为第三方模块会干扰根日志记录器。一般的做法是使用以所在文件命名的日志记录器,使用下面的代码:
logger = logging.getLogger(__name__)
日志的配置可以继承,所以可以为顶层模块定义一个父日志记录器,让其中的所有 Python 模块都继承这个配置。
在 settings.py 中为所有应用设置日志记录器的方式如下所示:
现在,accounts.models、accounts.views 和 accounts.authentication 等应用都从父日志记录器 accounts 中继承 logging.StreamHandler。
不过,受限于 Django 的项目结构,无法为整个项目定义一个顶层日志记录器(除非使用根日志记录器),所以必须为每个应用定义各自的日志记录器。
为日志行为编写测试的方法如下所示:
可以测试一下,确保测试了我们想测试的行为:
然后使用真正的实现方式:
进行测试,可以看到成功了。
至此,已经让测试固件既可以在本地使用也能在服务器中使用,还设定了更牢靠的日志配置。
谨慎去除功能测试中的重复
每个功能测试没必要都测试应用的全部功能。在功能测试中还可能需要跳过一些过程。但是,要提醒一下,功能测试的目的是为了捕获应用不同部分之间交互时的异常表现,所去除重复时不要太过火了。
测试固件
测试固件指运行测试之前要提前准备好的测试数据。一般使用一些数据填充数据库,不过如前所示(创建浏览器的 cookie),也会涉及到其他准备工作。
避免使用 JSON 固件
Django 提供的 dumpdata 和 loaddata 等命令,简化了把数据库中的数据导出为 JSON 格式的操作,也可以轻易的使用 JSON 格式数据还原数据库。大多数人都不建议使用这种固件,因为数据库模式发生变化后这种固件很难维护。所以建议使用 ORM,或者 factory_boy这类工具。
固件也要在远程服务器中使用
在本地运行测试,使用 LiveServerTestCase 即可轻松通过 Django ORM 与测试数据库交互。与过渡服务器中的数据库交互就没这么简单了。解决办法之一是使用 Django 管理命令。
使用以所在模块命名的日志记录器
根日志记录器是一个全局对象,Python 进程中加载的所有库都能访问,所以这个日志记录器不完全在你的控制之中。因此,要使用 logging.getLogger(__name__) 获取一个相对模块唯一的记录器,而且这个记录器继承自设定的顶层配置。
测试重要的日志消息
日志消息对于调试生产环境中的问题十分重要。如果某个日志消息很重要,必须保留在代码中,或许也有必要测试。根据经验,比 logging.INFO 等级高的日志消息都要测试。在测试目标模块所用的日志记录器上使用 patch.object,有助于简化日志消息的单元测试。