第 17 章 测试固件,日志和服务器端调试

理论上一个测试只应该测试一件事,所以没必要在每隔功能测试中都测试登录和退出功能。所以需要找到一种方法“作弊”,跳过认证,这样就不用花时间等待执行完重复的测试路径了。

在功能测试中去除重复时不要做得太过火了。功能测试的优势之一是,可以捕获应用不同部分之间交互时产生的神秘莫测的表现。

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 格式的固件arrow-up-right。如果修改了模型,这种固件维护起来像噩梦。因此,只要可以,就直接使用 Django ORM 加载数据,或者使用 factory_boyarrow-up-right 之类的工具。

检查是否可行

要检查这种做法是否可行,最好使用前面测试中定义的 wait_to_be_logged_in 函数。要想在不同的测试中访问这个方法,就要把它连同另外几个方法一起移到 FunctionalTest 类中。

相应调整一下 test_login.py

为了确认我们没有破坏现有功能,再次运行登录测试:

现在可以为 “My Lists” 页面编写一个占位测试,检查实现创建认证会话的做法是否可行:

测试结果为 OK,现在可以提交了:

17.2 实践时检验真理的唯一标准:在过渡服务器中捕获最后的问题

这个功能测试在本地运行一切正常,但在过渡服务器中遇到了意料之外的问题,为了解决这个问题,要找到在测试服务器中管理数据库的方法:

然后重启 Gunicron

接着运行功能测试:

无法登陆,不管真的是用 Persona 还是已经使用通过认证的会话都不行。这说明测试有问题。我们需要练习如何使用服务器端调试技术。

17.2.1 设置日志

为了记录这个问题,配置 Gunicorn,让它记录日志。在服务器中使用 vinano 按照下面的方式调整 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,让功能测试使用隧道连接的端口和数据库交互。

警告:小心,不要在线上服务器中运行测试

我们现在编写的代码能直接影响服务器中的数据库。一定要非常小心,别在错误的主机中运行功能测试,把生产数据库清空了。

此时,可以考虑使用一些安全防护措施。例如,把过渡环境和生产环境放在不同的服务器中,而且不同的服务器使用不同的口令认证密钥对。

在生产环境的数据副本中运行测试也有同样的危险。

17.4 集成日志相关的代码

接下来要把日志相关的代码集成到应用中。把输出日志的代码放在那儿,并且纳入版本控制,有助于调试以后遇到的登陆问题。

先把 Gunicorn 的配置保存到 deploy_tools 文件夹里的临时文件中。

使用可继承的日志配置

前面调用 logging.warning 时,使用的是根日志记录器。一般来说,这么做并不好,因为第三方模块会干扰根日志记录器。一般的做法是使用以所在文件命名的日志记录器,使用下面的代码:

logger = logging.getLogger(__name__)

日志的配置可以继承,所以可以为顶层模块定义一个父日志记录器,让其中的所有 Python 模块都继承这个配置。

settings.py 中为所有应用设置日志记录器的方式如下所示:

现在,accounts.modelsaccounts.viewsaccounts.authentication 等应用都从父日志记录器 accounts 中继承 logging.StreamHandler

不过,受限于 Django 的项目结构,无法为整个项目定义一个顶层日志记录器(除非使用根日志记录器),所以必须为每个应用定义各自的日志记录器。

为日志行为编写测试的方法如下所示:

可以测试一下,确保测试了我们想测试的行为:

然后使用真正的实现方式:

进行测试,可以看到成功了。

17.5 小结

至此,已经让测试固件既可以在本地使用也能在服务器中使用,还设定了更牢靠的日志配置。

固件和日志

  • 谨慎去除功能测试中的重复

    • 每个功能测试没必要都测试应用的全部功能。在功能测试中还可能需要跳过一些过程。但是,要提醒一下,功能测试的目的是为了捕获应用不同部分之间交互时的异常表现,所去除重复时不要太过火了。

  • 测试固件

    • 测试固件指运行测试之前要提前准备好的测试数据。一般使用一些数据填充数据库,不过如前所示(创建浏览器的 cookie),也会涉及到其他准备工作。

  • 避免使用 JSON 固件

    • Django 提供的 dumpdata 和 loaddata 等命令,简化了把数据库中的数据导出为 JSON 格式的操作,也可以轻易的使用 JSON 格式数据还原数据库。大多数人都不建议使用这种固件,因为数据库模式发生变化后这种固件很难维护。所以建议使用 ORM,或者 factory_boyarrow-up-right这类工具。

  • 固件也要在远程服务器中使用

    • 在本地运行测试,使用 LiveServerTestCase 即可轻松通过 Django ORM 与测试数据库交互。与过渡服务器中的数据库交互就没这么简单了。解决办法之一是使用 Django 管理命令。

  • 使用以所在模块命名的日志记录器

    • 根日志记录器是一个全局对象,Python 进程中加载的所有库都能访问,所以这个日志记录器不完全在你的控制之中。因此,要使用 logging.getLogger(__name__) 获取一个相对模块唯一的记录器,而且这个记录器继承自设定的顶层配置。

  • 测试重要的日志消息

    • 日志消息对于调试生产环境中的问题十分重要。如果某个日志消息很重要,必须保留在代码中,或许也有必要测试。根据经验,比 logging.INFO 等级高的日志消息都要测试。在测试目标模块所用的日志记录器上使用 patch.object,有助于简化日志消息的单元测试。

Last updated