为Zope和Plone项目提供测试基础设施。
项目描述
简介
plone.testing 为在 Zope 和 Plone 环境中编写单元和集成测试提供了工具。它与 Plone 无关,也不依赖于 Zope(尽管它有一些可选的 Zope 特性)。
plone.testing 基于 zope.testing,特别是其层概念。此软件包还旨在推广编写各种类型测试的“良好实践”。
如果您是自动化测试和测试驱动开发的初学者,您应该花一些时间了解这些概念。一些有用的参考资料包括
请记住,不同的 Python 框架在如何处理测试方面有所不同。因此,您可能会发现与下面显示的示例不同的示例。然而,核心概念应该是致的。
兼容性
plone.testing 7.x 已与 Python 2.7 和 3.6 进行测试。如果您使用可选的 Zope 层,您必须使用 Zope 4 或更高版本。查看旧的 plone.testing 版本以支持旧版本的 Zope。
定义
在本文档中,我们将使用许多与测试相关的术语。以下定义适用
- 单元测试
一个自动测试(即在代码中编写的测试),它测试单个单元(通常是函数)的独立。单元测试试图证明给定的函数在给定的输入下按预期工作并产生正确的输出。通常,一个函数会有许多单元测试,测试不同的输入,包括边界情况和错误。单元测试通常编写和运行都很快速。
- 集成测试
自动测试用于检验多个单元之间的交互。在Zope环境中,这通常涉及特定对象或视图与Zope框架、ZODB持久化引擎等的交互。集成测试通常需要一些设置,并且运行速度可能比单元测试慢。通常集成测试的数量少于单元测试。
- 功能测试
以“端到端”的方式测试一个特性的自动测试。在Zope环境中,这意味着它以用户相同的方式调用操作,即通过Web请求。功能测试通常比单元测试或集成测试运行得更慢,甚至可能显著更慢。因此,通常只为每个主要特性编写少数几个功能测试,并依赖单元测试和集成测试进行大部分测试。
- 黑盒测试
只考虑系统定义的输入和输出的测试。例如,功能测试通常是一个黑盒测试,它只通过定义的接口(例如,在Web应用程序中发布的URL)提供输入,并对最终输出(例如,对那些URL的请求返回的响应)进行断言。
- 白盒测试
通过检查系统的内部状态来做出断言的测试。单元测试和集成测试的作者通常对被测试代码的实现有相当的了解,并且可以检查数据库中的数据或系统环境的更改,以确定测试是成功还是失败。
- 断言
检查测试是成功还是失败。例如,如果针对函数foo()的单元测试期望它返回值1,则可以编写一个断言来验证这一点。如果一个测试的任何断言失败,则该测试被认为失败。每个测试总是包含一个或多个断言。
- 测试用例
单个单元、集成或功能测试。通常简称为测试。测试用例设置、执行并对单个待测试场景进行断言。
- 测试设置
作为一个或多个测试基准状态。测试设置在每次测试执行之前进行设置,并在之后拆除。这是测试隔离的前提条件——即测试应该是相互独立的。
- 层
多个测试共享的测试设置配置。属于特定层的所有测试用例将一起执行。层在测试执行之前进行一次设置,并在之后拆除一次。层可能相互依赖。任何基本层在特定子层使用之前进行设置,并在使用后拆除。测试运行程序将按顺序排列测试执行,以最小化层的设置和拆除。
- 测试套件
一起执行的测试用例(和层)的集合。
- 测试运行程序
执行测试的程序。这负责调用层和测试设置设置和拆除方法。它还报告测试运行情况,通常通过打印输出到控制台。
- 覆盖率
为了对代码有信心,你应该确保它得到了充分的测试覆盖。也就是说,每一行代码和每个可能的分支点(循环、if语句)都应该由测试执行。这被称为覆盖率,通常以非测试代码行数被测试覆盖的百分比来衡量。覆盖率可以通过测试运行程序来衡量,它跟踪给定测试运行中执行的代码行。
- Doctest
一种测试风格,其中测试被编写为可以输入到交互式Python解释器中的示例。测试运行器执行每个示例,并将实际输出与预期输出进行比较。Doctests可以放在方法文档字符串中,或者放在单独的文件中。使用doctests主要是个人的偏好。一些开发者喜欢将文档作为doctests编写,这有优点,即代码示例可以自动测试其正确性。您可以在维基百科上了解更多关于doctests的信息。
安装和使用
要在您自己的包中使用plone.testing,您需要将其作为依赖项添加。大多数人更喜欢将仅测试的依赖项保持分离,这样在不会运行测试的场景(例如生产服务器上)就不需要安装它们。这可以通过使用test额外项来实现。
在setup.py中,添加或修改extras_require选项,如下所示:
extras_require = { 'test': [ 'plone.testing', ] },
当然,您也可以将其他仅测试的依赖项添加到该列表中。
要运行测试,您需要一个测试运行器。如果您正在使用zc.buildout,您可以使用zc.recipe.testrunner配方安装测试运行器。例如,您可以将以下内容添加到您的buildout.cfg中:
[test] recipe = zc.recipe.testrunner eggs = my.package [test] defaults = ['--auto-color', '--auto-progress']
当然,您还需要将其添加到parts列表中。
[buildout] parts = ... test
在这个例子中,我列出了一个要测试的单一包,称为my.package,并要求使用[test]额外项安装它。这将安装任何常规依赖项(在setup.py中的install_requires选项中列出),以及与test键关联的列表中的依赖项。
请注意,在这里正确列出您的依赖项变得很重要,因为测试运行器只会了解明确列出的包及其依赖项。例如,如果您的包依赖于Zope,您需要在setup.py中的install_requires列表中列出Zope;同样适用于Plone,或实际上任何您从中导入的包。
重新运行buildout后,测试运行器将作为bin/test(可执行文件名来自buildout部分的名称)安装。您可以通过不带参数执行它来运行eggs列表中每个egg的所有测试。
$ bin/test
如果您列出了多个egg,并且您只想运行特定egg的测试,您可以这样做
$ bin/test -s my.package
如果您只想运行此包中的特定测试,请使用-t选项。这可以传递一个匹配doctest文件名或测试方法名的正则表达式。
$ bin/test -s my.package -t test_spaceship
还有其他命令行选项,您可以通过运行来找到
$ bin/test --help
请注意,在buildout配置中还有defaults选项。这可以用来设置默认命令行选项。上面显示了一些常用的选项。
覆盖率报告
在编写测试时,了解您的测试覆盖了多少代码是很有用的。您可以通过出色的coverage库创建覆盖率报告。为了使用它,我们需要安装它和一个报告脚本
[buildout] parts = ... test coverage report [coverage] recipe = zc.recipe.egg eggs = coverage initialization = include = '--source=${buildout:directory}/src' sys.argv = sys.argv[:] + ['run', include, 'bin/test', '--all'] [report] recipe = zc.recipe.egg eggs = coverage scripts = coverage=report initialization = sys.argv = sys.argv[:] + ['html', '-i']
这将运行带有类似 –all 参数的 bin/test 脚本以运行所有层。您也可以指定没有或一些其他参数。它将在您的构建根目录中的 .coverage 文件中放置覆盖率报告信息。通过 --source 参数,您可以指定包含您想覆盖的代码的目录。否则,覆盖率脚本将生成所有执行代码的覆盖率信息,包括其他包甚至标准库。
运行 bin/report 脚本将在 htmlcov 目录中生成运行结果的人类可读的HTML表示。在浏览器中打开包含的 index.html 以查看结果。
如果您想生成适用于 Cobertura 插件的 Jenkins 的 XML 表示,您可以添加另一个部分
[buildout] parts = ... report-xml [report-xml] recipe = zc.recipe.egg eggs = coverage scripts = coverage=report-xml initialization = sys.argv = sys.argv[:] + ['xml', '-i']
这将生成构建根目录中的 coverage.xml 文件。
可选依赖
plone.testing 提供了一组用于管理层的核心工具,它只依赖于 zope.testing。此外,还有几个层和辅助函数,可以在您的测试中使用(或作为您自己的层的基)。其中一些有更深层次的依赖。然而,这些依赖项是可选的,并且默认不安装。如果您不使用相关的层,您可以安全地忽略它们。
plone.testing 指定了这些依赖项,但是使用 setuptools 的“额外”功能。您可以在自己的 setup.py install_requires 或 extras_require 选项中使用相同的方括号符号来依赖一个或多个额外功能,正如上面显示的 [test] 构建部分。例如,如果您需要 zca 和 publisher 额外功能,您可以在您的 setup.py 中有如下所示的内容
extras_require = { 'test': [ 'plone.testing [zca, publisher]', ] },
可用的额外功能有
- zodb
ZODB 测试。依赖于 ZODB。相关的层和辅助函数位于 plone.testing.zodb 模块中。
- zca
Zope 组件架构测试。依赖于核心 Zope 组件架构包,如 zope.component 和 zope.event。相关的层和辅助函数位于 plone.testing.zca 模块中。
- security
安全测试。依赖于 zope.security。相关的层和辅助函数位于 plone.testing.security 模块中。
- publisher
Zope Publisher 测试。依赖于 zope.publisher、zope.browsermenu、zope.browserpage、zope.browserresource 和 zope.security,并设置 ZCML 指令。相关的层和辅助函数位于 plone.testing.publisher 模块中。
zope (为了向后兼容,还有 z2。)
Zope 测试。依赖于包含 Zope 应用程序服务器所有依赖项的 Zope egg。相关的层和辅助函数位于 plone.testing.zope 模块中。
zserver
针对 ZServer 的测试。(仅适用于 Python 2!)需要额外使用 zope 额外功能。相关的层和辅助函数位于 plone.testing.zserver 模块中。
将测试构建包添加到您的软件包中
在创建可重用的、大多数情况下是独立包时,通常非常有用能够包含包含包源代码的 buildout 本身,这可以用来创建测试运行器。这对于许多 Zope 包来说是一种流行的方法。事实上,plone.testing 本身也使用了这种布局。
要在您的包中拥有一个自包含的构建,需要以下要求
您需要在包的根目录下有一个 buildout.cfg 文件。
在大多数情况下,您总是想要一个 bootstrap.py 文件,以便人们更容易设置新的构建。
您的包源需要位于一个 src 目录中。如果您正在使用命名空间包,这意味着顶层包应该在 src 目录中。
src 目录必须在 setup.py 中引用。
例如,plone.testing 的布局如下所示
plone.testing/ plone.testing/setup.py plone.testing/bootstrap.py plone.testing/buildout.cfg plone.testing/README.rst plone.testing/src/ plone.testing/src/plone plone.testing/src/plone/__init__.py plone.testing/src/plone/testing/ plone.testing/src/plone/testing/*
在 setup.py 中,需要以下参数
packages=find_packages('src'), package_dir={'': 'src'},
这告诉 setuptools 在哪里可以找到源代码。
plone.testing 的 buildout.cfg 看起来像这样
[buildout] extends = http://download.zope.org/Zope2/index/2.12.12/versions.cfg parts = coverage test report report-xml develop = . [test] recipe = collective.xmltestreport eggs = plone.testing [test] defaults = ['--auto-color', '--auto-progress'] [coverage] recipe = zc.recipe.egg eggs = coverage initialization = include = '--source=${buildout:directory}/src' sys.argv = sys.argv[:] + ['run', include, 'bin/test', '--all', '--xml'] [report] recipe = zc.recipe.egg eggs = coverage scripts = coverage=report initialization = sys.argv = sys.argv[:] + ['html', '-i'] [report-xml] recipe = zc.recipe.egg eggs = coverage scripts = coverage=report-xml initialization = sys.argv = sys.argv[:] + ['xml', '-i']
显然,您应该根据需要调整 eggs 列表中的包名和 extends 行中设置的版本。
当然,您还可以添加额外的构建部分,例如包括一些开发/调试工具,或者甚至是一个用于测试目的的运行中的应用服务器。
提示:如果您使用此包布局,请避免将构建生成的任何文件或目录检查到版本控制仓库中。您希望忽略
.coverage
.installed.cfg
bin
coverage.xml
develop-eggs
htmlcov
parts
src/*.egg-info
层
在很大程度上,plone.testing 是关于层的。它提供
一组层(如下所示),您可以使用或扩展。
一组用于处理层的工具
一个微型框架,使编写层和管理与层相关联的共享资源变得容易。
在展示如何编写使用层的测试之前,我们将讨论最后两个项目。
层基础知识
层用于创建多个测试用例共享的测试固定值。例如,如果您正在编写一组集成测试,您可能需要设置数据库并配置各种组件以访问该数据库。这种类型的测试固定值设置可能非常消耗资源和时间。如果可以只为一系列测试执行一次设置和拆除操作,而不失去这些测试之间的隔离,则测试运行通常可以显著加快。
层还允许重用测试固定值和设置/拆除代码。 plone.testing 提供了多个有用的(但可选的)层,用于管理常见 Zope 测试场景的测试固定值,让您可以专注于实际的测试编写。
在最基本的情况下,层是一个具有以下方法和属性的对象
- setUp()
当层需要设置时,由测试运行器调用。对于测试运行期间使用的每个层,此调用只执行一次。
- tearDown()
当层需要拆除时,由测试运行器调用。与 setUp() 一样,此调用对于每个层只执行一次。
- testSetUp()
在执行每个使用该层的测试用例之前立即调用。这对于设置在每项测试的基础上管理的固定值方面很有用,而不是在所有测试中共享的固定值。
- testTearDown()
在执行每个使用该层的测试用例之后立即调用。这是一个执行任何测试后清理的机会,以确保固定值可以为下一个测试做好准备。
- __bases__
一组基础层。
每个测试用例都与零层或一层关联。(指定层的语法在下文的“编写测试”部分中展示。)与给定层关联的所有测试都将一起执行。
层可以相互依赖(如__bases__元组所示),允许一个层构建另一个层创建的设置。基本层在其依赖项设置之前设置,在依赖项拆除之后拆除。
例如,如果测试运行器正在执行属于层A的一些测试以及属于层B的一些其他测试,这两个层都依赖于层C,则执行顺序可能为
1. C.setUp() 1.1. A.setUp() 1.1.1. C.testSetUp() 1.1.2. A.testSetUp() 1.1.3. [One test using layer A] 1.1.4. A.testTearDown() 1.1.5. C.testTearDown() 1.1.6. C.testSetUp() 1.1.7. A.testSetUp() 1.1.8. [Another test using layer A] 1.1.9. A.testTearDown() 1.1.10. C.testTearDown() 1.2. A.tearDown() 1.3. B.setUp() 1.3.1. C.testSetUp() 1.3.2. B.testSetUp() 1.3.3. [One test using layer B] 1.3.4. B.testTearDown() 1.3.5. C.testTearDown() 1.3.6. C.testSetUp() 1.3.7. B.testSetUp() 1.3.8. [Another test using layer B] 1.3.9. B.testTearDown() 1.3.10. C.testTearDown() 1.4. B.tearDown() 2. C.tearDown()
当然,基本层可能依赖于其他基本层。在这种情况下,测试运行器计算设置和拆除顺序的方式类似于Python在多重继承情况下搜索要调用的方法。
编写层
创建新层的最简单方法是使用Layer基本类,并根据需要实现setUp()、tearDown()、testSetUp()和testTearDown()方法。所有四个都是可选的。每个方法的默认实现都不做任何操作。
按照惯例,层在顶级包中的testing.py模块中创建。这样做的目的是,扩展您的包的其他包可以重用您的层进行自己的测试。
一个简单的层可能看起来像这样
>>> from plone.testing import Layer >>> class SpaceShip(Layer): ... ... def setUp(self): ... print("Assembling space ship") ... ... def tearDown(self): ... print("Disasembling space ship") ... ... def testSetUp(self): ... print("Fuelling space ship in preparation for test") ... ... def testTearDown(self): ... print("Emptying the fuel tank")
在可以使用此层之前,必须对其进行实例化。由于层本质上在测试之间共享,因此层通常只实例化一次。当开始在层中管理资源(如持久数据、数据库连接或其他共享资源)时,这一点变得很重要。
按照惯例,层实例也位于testing.py中,位于层类定义之后。
>>> SPACE_SHIP = SpaceShip()
上面显示的层没有基本层(依赖项)。下面是另一个依赖于它的层的示例:
>>> class ZIGSpaceShip(Layer): ... defaultBases = (SPACE_SHIP,) ... ... def setUp(self): ... print("Installing main canon") >>> ZIG = ZIGSpaceShip()
在这里,我们已明确列出ZIGSpaceShip依赖的基本层,在defaultBases属性中。这是由Layer基本类用来设置层基础的,同时也可以覆盖:请参见下文。
请注意,我们在defaultBases元组中使用层实例,而不是类。层依赖始终与特定的层实例相关。上面,我们实际上是在说,ZIGSpaceShip的实例默认情况下将需要先设置SPACE_SHIP层。
高级 - 覆盖基础层
在某些情况下,创建层的副本并更改其基础可能是有用的。这样做的一个原因可能是在重用另一个模块中的层时,需要更改层的设置和拆解的顺序。
当然,通常您会重用层实例,无论是在测试中直接使用还是在另一个层的 defaultBases 元组中,但如果需要更改基础,可以将新的基础列表传递给层实例构造函数:
>>> class CATSMessage(Layer): ... ... def setUp(self): ... print("All your base are belong to us") ... ... def tearDown(self): ... print("For great justice") >>> CATS_MESSAGE = CATSMessage() >>> ZERO_WING = ZIGSpaceShip(bases=(SPACE_SHIP, CATS_MESSAGE,), name="ZIGSpaceShip:CATSMessage")
请注意,以这种方式覆盖基础时,需要 name 参数。这是因为每个层(在给定的测试运行中使用)都必须有一个唯一的名称。默认情况下,使用层类名称,但这显然只适用于一次实例化。因此,plone.testing 在显式设置 bases 时需要名称。
请非常小心地更改层基础。层实现可能对其基础设置的测试环境有假设。如果您更改基础列出的顺序或完全删除一个基础,层可能无法正确设置。
此外,请注意,新的层实例与原始层实例是独立的,因此层中定义的任何资源可能会被重复。
层组合
有时,将几个层组合成一个,而不添加任何新的环境可能是有用的。实现这一目标的一种方法是通过直接使用 Layer 类并使用新的基础进行实例化:
>>> COMBI_LAYER = Layer(bases=(CATS_MESSAGE, SPACE_SHIP,), name="Combi")
在此,我们创建了一个具有两个基础 CATS_MESSAGE 和 SPACE_SHIP 的“无操作”层,命名为 Combi。
请注意,直接像这样使用 层 时,需要提供 名称 参数。这是为了使测试运行程序能够正确识别层。通常,层的类名被用作名称的基础,但直接使用 层 基类时,这不太可能是唯一的或描述性的。
层资源
许多层将管理一个或多个资源,这些资源可能被其他层使用,或者由测试本身使用。示例可能包括数据库连接、线程局部对象或配置数据。
plone.testing 包含一个简单的资源存储抽象,使得从依赖层或测试中访问资源变得容易。资源存储使用字典表示法:
>>> class WarpDrive(object): ... """A shared resource""" ... ... def __init__(self, maxSpeed): ... self.maxSpeed = maxSpeed ... self.running = False ... ... def start(self, speed): ... if speed > self.maxSpeed: ... print("We need more power!") ... else: ... print("Going to warp at speed", speed) ... self.running = True ... ... def stop(self): ... self.running = False >>> class ConstitutionClassSpaceShip(Layer): ... defaultBases = (SPACE_SHIP,) ... ... def setUp(self): ... self['warpDrive'] = WarpDrive(8.0) ... ... def tearDown(self): ... del self['warpDrive'] >>> CONSTITUTION_CLASS_SPACE_SHIP = ConstitutionClassSpaceShip() >>> class GalaxyClassSpaceShip(Layer): ... defaultBases = (CONSTITUTION_CLASS_SPACE_SHIP,) ... ... def setUp(self): ... # Upgrade the warp drive ... self.previousMaxSpeed = self['warpDrive'].maxSpeed ... self['warpDrive'].maxSpeed = 9.5 ... ... def tearDown(self): ... # Restore warp drive to its previous speed ... self['warpDrive'].maxSpeed = self.previousMaxSpeed >>> GALAXY_CLASS_SPACE_SHIP = GalaxyClassSpaceShip()
如图所示,层(从 plone.testing.Layer 继承而来)支持项(类似字典)的赋值、访问和删除字符串键下的任意资源。
重要:如果层在设置固定值期间创建资源(如上所示,通过将对象分配给 self 上的键),它还必须在拆卸时删除资源。设置和删除应该是对称的:如果资源在 setUp() 期间分配,则应在 tearDown() 中删除;如果它在 testSetUp() 中创建,则应在 testTearDown() 中删除。
在基本层中定义的资源可以从子层访问和传递。如果子层使用一个在基本层中也存在的键设置资源,子层版本将覆盖基本版本,直到子层被拆卸(假设它删除了资源,它应该这样做),但基本层版本保持完整。
相反,如果(如上所示)子层访问并修改对象,它将修改原始对象。
如果在多个基本层中使用相同的键,选择要使用的版本规则与在选择多个继承情况下使用属性或方法时应用的规则类似。
在上面的例子中,我们使用了 warpDrive 对象的资源管理器,但我们把 previousMaxSpeed 变量分配给了 self。这是因为 previousMaxSpeed 是层内部的,不应与其他任何可能使用此层作为基础的层共享。它也不应被任何测试用例使用。相反,warpDrive 是一个暴露给其他层和测试用例的共享资源。
当考虑测试用例如何访问共享资源时,这种区别就更加重要了。我们将在稍后讨论如何编写使用层的测试用例,但考虑以下测试:
>>> import unittest >>> class TestFasterThanLightTravel(unittest.TestCase): ... layer = GALAXY_CLASS_SPACE_SHIP ... ... def test_hyperdrive(self): ... warpDrive = self.layer['warpDrive'] ... warpDrive.start(8)
这个测试需要访问共享资源。它知道它的层定义了一个名为warpDrive的资源。它不知道或不关心这个warp drive实际上是由ConstitutionClassSpaceShip基础层启动的。
然而,如果基础层将资源分配为实例变量,它就不会继承到子层(记住:层基不是基类!)。访问它的语法是:
self.layer.__bases__[0].warpDrive
这不仅难看,而且脆弱:如果改变基类的列表,上面的表达式可能会导致属性错误。
编写测试
测试通常以两种方式编写:作为从unittest.TestCase派生的类的成员方法(这有时被称为“Python测试”或“JUnit风格的测试”),或者使用doctest语法。
你应该意识到,尽管相关的框架(unittest和doctest)经常谈论单元测试,但这些工具也用于编写集成和功能测试。单元测试、集成测试和功能测试之间的区别主要在于实践:你使用相同的技巧来设置测试夹具或为集成测试编写断言,就像你为单元测试做的那样。区别在于夹具包含的内容以及如何调用待测试的代码。一般来说,真正的单元测试将具有最小或没有测试夹具,而集成测试将具有包含与你的代码集成的组件的夹具。功能测试将具有包含足够完整的系统以执行和测试“端到端”场景的夹具。
Python 测试
Python测试使用Python的unittest模块。测试应该放在名为tests的模块或包中,以便测试运行器可以找到它们。
对于小型包,一个名为tests.py的单个模块通常会包含所有测试。对于大型包,通常有一个包含多个测试模块的tests包。这些模块需要以单词test开头,例如tests/test_foo.py或tests/test_bar.py。也不要忘记在tests包中包含__init__.py!
unittest
请注意,在编写本文时(版本4.6.2),zope.testing测试运行器不支持来自unittest的新setUpClass()、tearDownClass()、setUpModule()和tearDownModule()钩子。这通常不是问题,因为我们倾向于使用层来管理复杂的夹具,但了解这一点很重要。
测试模块、类和函数
Python测试是用从基础类TestCase派生的类编写的。每个测试都编写为一个不接受任何参数且名称以test开头的方法。可以添加其他方法,并从测试方法中适当调用它们,例如,为了共享一些测试逻辑。
还可以添加两个特殊方法,setUp()和tearDown()。这些方法分别在每个测试之前或之后调用,并提供了一个有用的地方来构建和清理测试夹具,而无需编写自定义层。尽管如此,它们显然没有层那么可重用。
提示:有些令人困惑的是,测试用例类中的setUp()和tearDown()方法与层类中testSetUp()和testTearDown()方法的等效。
可以通过设置 layer 类属性为一个层实例来指定一个层。如果层与测试类本身的 setUp() 和 tearDown() 方法一起使用,则类的 setUp() 方法将在层的 testSetUp() 方法之后被调用,而类的 tearDown() 方法将在层的 testTearDown() 方法之前被调用。
TestCase 基类包含了一些方法,这些方法可用于编写断言。它们都采用 self.assertSomething() 的形式,例如 self.assertEqual(result, expectedValue)。有关详细信息,请参阅 unittest 文档。
将这些内容结合起来,让我们扩展先前的单元测试示例:
>>> import unittest >>> class TestFasterThanLightTravel(unittest.TestCase): ... layer = GALAXY_CLASS_SPACE_SHIP ... ... def setUp(self): ... self.warpDrive = self.layer['warpDrive'] ... self.warpDrive.stop() ... ... def tearDown(self): ... self.warpDrive.stop() ... ... def test_warp8(self): ... self.warpDrive.start(8) ... self.assertEqual(self.warpDrive.running, True) ... ... def test_max_speed(self): ... tooFast = self.warpDrive.maxSpeed + 0.1 ... self.warpDrive.start(tooFast) ... self.assertEqual(self.warpDrive.running, False)
以下是一些需要注意的事项
该类从 unittest.TestCase 继承。
layer 类属性被设置为先前定义的层实例(不是层类!)。这通常是从 testing 模块中导入的。
这里有两个测试:test_warp8() 和 test_max_speed()。
我们在两个测试中都使用了 self.assertEqual() 断言来检查在 warp 驱动器上执行 start() 方法的结果。
我们使用了 setUp() 方法来获取 warpDrive 资源,并在执行每个测试之前确保它已停止。将变量分配给 self 是向每个测试方法提供一些状态的有用方式,但请注意测试之间的数据泄露:一般来说,您无法预测测试的运行顺序,并且测试应该是独立的。
我们使用了 tearDown() 方法来确保在每个测试之后 warp 驱动器确实已停止。
测试套件
像上面的类就足够了:任何从 TestCase 继承的类,只要它的模块名称以 test 开头,都会被检查是否有测试方法。然后,这些测试被收集到一个测试套件中并执行。
有关其他选项,请参阅 unittest 文档。
Doctests
Doctests 可以以两种方式编写:作为文档字符串的内容(通常,但并非总是,作为说明和测试文档字符串中出现的功能或类的方法的一种手段),或者作为单独的文本文件。在两种情况下,都使用标准的 doctest 模块。有关 doctest 语法和约定,请参阅其文档。
Doctests 可以以两种不同的方式使用
用于测试文档。也就是说,确保文档中包含的代码示例是有效的,并且随着软件的更新仍然可以正常工作。
作为编写测试的方便语法。
这两种方法使用相同的测试 API 和技术。区别主要在于思维模式。然而,避免陷入测试可以替代良好文档或反之亦然的陷阱是很重要的。测试通常需要系统地遍历输入和输出,并覆盖多个边缘情况。文档应该讲述一个引人入胜的故事,并且通常专注于主要的使用场景。试图一举两得通常只会让您得到一个不吸引人的石头和羽毛的堆。
文档字符串 Doctests
可以在任何模块、类或函数的文档字符串中添加 Doctests:
def canOutrunKlingons(warpDrive): """Find out of the given warp drive can outrun Klingons. Klingons travel at warp 8 >>> drive = WarpDrive(5) >>> canOutrunKlingons(drive) False We have to be faster than that to outrun them. >>> drive = WarpDrive(8.1) >>> canOutrunKlingons(drive) True We can't outrun them if we're travelling exactly the same speed >>> drive = WarpDrive(8.0) >>> canOutrunKlingons(drive) False """ return warpDrive.maxSpeed > 8.0
要将特定模块的 doctests 添加到测试套件中,您需要使用 test_suite() 函数钩子:
>>> import doctest >>> def test_suite(): ... suite = unittest.TestSuite() ... suite.addTests([ ... unittest.makeSuite(TestFasterThanLightTravel), # our previous test ... doctest.DocTestSuite('spaceship.utils'), ... ]) ... return suite
在这里,我们将要检查的模块名称作为一个字符串点分名称给出。也可以导入一个模块并将其作为对象传递。上面的代码将一个列表传递给 addTests(),这使得向测试套件添加多个测试集变得容易:列表可以从对 DocTestSuite()、DocFileSuite()(如下所示)和 makeSuite()(如下所示)的调用中构建。
请记住,如果你在一个既有 TestCase-派生的 Python 测试的模块中添加了 test_suite() 函数,那么这些测试将不会自动被 zope.testing 捕获,因此你需要显式地将它们添加到测试套件中。
上面的例子说明了面向文档的 doctest,其中 doctest 是公共模块文档字符串的一部分。相同的语法也可以用于更系统的单元测试。例如,我们可以有一个名为 spaceship.tests.test_spaceship 的模块,其中包含一系列方法
# It's often better to put the import into each method, but here we've # imported the code under test at module level from spaceship.utils import WarpDrive, canOutrunKlingons def test_canOutrunKlingons_too_small(): """Klingons travel at warp 8.0 >>> drive = WarpDrive(7.9) >>> canOutrunKlingons(drive) False """ def test_canOutrunKlingons_big(): """Klingons travel at warp 8.0 >>> drive = WarpDrive(8.1) >>> canOutrunKlingons(drive) True """ def test_canOutrunKlingons_must_be_greater(): """Klingons travel at warp 8.0 >>> drive = WarpDrive(8.0) >>> canOutrunKlingons(drive) False """
在这里,我们创建了一系列没有主体的微小方法。它们仅仅作为包含带有 doctests 的文档字符串的容器。由于模块没有全局变量,每个测试都必须导入要测试的代码,这有助于使导入错误更加明确。
文件 Doctests
包含在文件中的 doctests 与包含在文档字符串中的 doctests 类似。文件 doctests 更适合于涵盖整个模块或包使用的叙述性文档。
例如,如果我们有一个名为 spaceship.txt 的文件包含 doctests,我们可以通过以下方式将其添加到上面的测试套件中:
>>> def test_suite(): ... suite = unittest.TestSuite() ... suite.addTests([ ... unittest.makeSuite(TestFasterThanLightTravel), ... doctest.DocTestSuite('spaceship.utils'), ... doctest.DocFileSuite('spaceship.txt'), ... ]) ... return suite
默认情况下,文件位于定义测试套件的模块相对位置。你可以使用 ../(甚至在 Windows 上)来引用父目录,这在 doctest 在 tests 包中的模块内部时有时很有用。
可以将多个测试传递给套件,例如
>>> def test_suite(): ... suite = unittest.TestSuite() ... suite.addTests([ ... unittest.makeSuite(TestFasterThanLightTravel), ... doctest.DocTestSuite('spaceship.utils'), ... doctest.DocFileSuite('spaceship.txt', 'warpdrive.txt',), ... ]) ... return suite
测试运行器将每个文件报告为一个单独的测试,即上面的 DocFileSuite() 会将两个测试添加到整个套件中。相反,使用包含一个以上文档字符串的 doctests 的 DocTestSuite() 将为每个合格的文档字符串报告一个测试。
Doctest 固定和层
默认情况下,文档字符串 doctest 可以访问在找到文档字符串的模块中可用的任何全局符号(例如,模块中定义或导入的任何内容)。可以通过将 globs 关键字参数传递给 DocTestSuite() 构造函数来覆盖全局命名空间,或者通过传递 extraglobs 参数来增强它。两者都应给出字典。
文件 doctest 默认有一个空的全局命名空间。可以通过将 globs 参数传递给 DocFileSuite() 来提供全局变量。
为了管理 doctest 的简单测试用例,你可以定义设置和清理函数,并将它们分别作为 setUp 和 tearDown 参数传递。这两个函数都传递一个单一参数,即 DocTest 对象。该对象最有用的属性是 globs,它是一个在测试中可用的全局变量可变的字典。
例如:
>>> def setUpKlingons(doctest): ... doctest.globs['oldStyleKlingons'] = True >>> def tearDownKlingons(doctest): ... doctest.globs['oldStyleKlingons'] = False >>> def test_suite(): ... suite = unittest.TestSuite() ... suite.addTests([ ... doctest.DocTestSuite('spaceship.utils', setUp=setUpKlingons, tearDown=tearDownKlingons), ... ]) ... return suite
相同的参数在 DocFileSuite() 构造函数中也是可用的。对于 DocTestSuite,在给定模块中的每个文档字符串之前都会调用设置方法,在 DocFileSuite 中则是在每个提供的文件之前。
当然,我们通常也想要在 doctests 中使用层。不幸的是,unittest API 并不知道层,因此您不能直接将层传递给 DocTestSuite() 和 DocFileSuite() 构造函数。相反,您必须在构造后设置套件上的 layer 属性。
此外,为了在 doctest 中使用层资源,我们需要访问层实例。最简单的方法是将它作为全局名称“层”传递,通常称为‘层’。这使 doctest 本身可用全局名称‘层’,从而可以访问测试的层实例。
为了便于这样做,plone.testing 包含一个名为 layered() 的辅助函数。它的第一个参数是测试套件,第二个参数是层。
例如:
>>> from plone.testing import layered >>> def test_suite(): ... suite = unittest.TestSuite() ... suite.addTests([ ... layered(doctest.DocTestSuite('spaceship.utils'), layer=CONSTITUTION_CLASS_SPACE_SHIP), ... ]) ... return suite
这相当于:
>>> def test_suite(): ... suite = unittest.TestSuite() ... ... spaceshipUtilTests = doctest.DocTestSuite('spaceship.utils', globs={'layer': CONSTITUTION_CLASS_SPACE_SHIP}) ... spaceshipUtilTests.layer = CONSTITUTION_CLASS_SPACE_SHIP ... suite.addTest(spaceshipUtilTests) ... ... return suite
(在这个例子中,我们选择使用 addTest() 添加单个套件,而不是使用 addTests() 一次性添加多个套件)。
Zope 测试工具
本文件中迄今为止所描述的所有内容都仅依赖于标准 unittest 和 doctest 模块以及 zope.testing,您可以使用此包而不需要任何其他依赖项。
然而,此包以及其他包中还有一些(和层)工具,特别适用于测试使用各种 Zope 相关框架的应用程序。
测试清理
如果一个测试使用全局注册表,那么在测试固定设施的设置和拆卸过程中可能需要清理该注册表。zope.testing 提供了一种注册清理处理程序的机制 - 调用以清理全局状态的方法。这可以在测试用例的 setUp() 和 tearDown() 固定设施生命周期方法中调用。
>>> from zope.testing import cleanup
假设我们有一个全局注册表,实现为一个字典:
>>> SOME_GLOBAL_REGISTRY = {}
如果我们想在每次测试运行时清理它,我们可以在字典上调用 clear()。由于这是一个无参数的方法,它是一个完美的清理处理程序。
>>> cleanup.addCleanUp(SOME_GLOBAL_REGISTRY.clear)
现在我们可以使用 cleanUp() 方法执行所有注册的清理:
>>> cleanup.cleanUp()
此调用可以放在测试类的 setUp() 和/或 tearDown() 方法中,例如。
事件测试
您可能希望测试一些使用 zope.event 触发特定事件的代码。zope.component 提供了一些捕获和分析事件的辅助工具。
>>> from zope.component import eventtesting
要使用它,您首先需要设置事件测试。以下显示的一些层将为您完成此操作,但您也可以通过调用 eventtesting.setUp() 方法自己完成,例如从您自己的 setUp() 方法:
>>> eventtesting.setUp()
这仅注册了一些通用的事件处理程序。一旦执行了预期会触发事件的代码,您就可以使用 getEvents() 辅助函数获取捕获的事件实例列表:
>>> events = eventtesting.getEvents()
现在您可以检查 events 来查看自上次清理以来捕获了哪些事件。
getEvents() 函数接受两个可选参数,用于筛选返回的事件列表。第一个参数 (event_type) 是一个接口。如果指定了,则只返回提供此接口的事件。第二个参数 (filter) 是一个带有一个参数的可调用对象。如果指定了,它将用每个捕获的事件调用。只有当筛选函数返回 True 的事件才会被包括。
eventtesting 模块按照上述方法注册了一个清理操作。当你调用 cleanup.cleanUp()(或它注册的处理程序 eventtesting.clearEvents()),事件列表将被清空,以便进行下一个测试。这里我们将手动进行:
>>> eventtesting.clearEvents()
模拟请求
许多测试需要请求对象,通常需要设置特定的请求/表单变量。 zope.publisher 包含了一个用于此目的的有用类。
>>> from zope.publisher.browser import TestRequest
可以使用无参数构造一个简单的测试请求:
>>> request = TestRequest()
要添加一个正文输入流,将 StringIO 或文件作为第一个参数传递。要设置环境(请求头),使用 environ 关键字参数。要模拟提交的表单,使用 form 关键字参数:
>>> request = TestRequest(form=dict(field1='foo', field2=1))
请注意,form 字典包含序列化的表单字段,因此不应在字段名称中包含如 :int 或 :boolean 这样的修饰符,并且值应转换为适当的类型。
注册组件
许多测试配置文件将依赖于至少注册了 Zope 组件架构 (ZCA) 组件。在正常操作中,这些可能通过 ZCML 注册,但在单元测试中,你应该避免加载包(及其依赖项)的完整 ZCML 配置。
相反,你可以使用 zope.component 中的 Python API 立即注册全局组件。最常用的三个函数是:
>>> from zope.component import provideAdapter >>> from zope.component import provideUtility >>> from zope.component import provideHandler
有关如何使用这些函数的详细信息,请参阅 zope.component 文档。
在注册此类全局组件时,避免测试泄漏非常重要。上述概述的 cleanup 机制可以在每个测试之间拆除组件注册表。还可以参考下面的 plone.testing.zca.UNIT_TESTING 层,它通过 testSetUp()/testTearDown() 机制自动执行此清理。
或者,你可以使用 plone.testing.zca.pushGlobalRegistry() 和 plone.testing.zca.popGlobalRegistry() 辅助函数“堆叠”一个新的全局组件注册表。这使得能够为给定的层设置和拆除特定组件,甚至允许测试安全地调用全局组件 API(或加载 ZCML - 见下文)并正确拆除。有关详细信息,请参阅下面的层参考。
加载 ZCML
集成测试通常需要加载 ZCML 配置。这可以使用 zope.configuration API 实现。
>>> from zope.configuration import xmlconfig
xmlconfig 模块包含两种加载 ZCML 的方法。
xmlconfig.string() 可以用来加载 ZCML 的字面字符串:
>>> xmlconfig.string("""\ ... <configure xmlns="http://namespaces.zope.org/zope" package="plone.testing"> ... <include package="zope.component" file="meta.zcml" /> ... </configure> ... """) <zope.configuration.config.ConfigurationMachine object at ...>
请注意,我们需要使用 <configure /> 元素的 package 属性显式设置一个包(用于相对导入和文件位置)。
请注意,除非传递了可选的第二个参数(context),否则每次调用string()时都会创建一个新的配置机器。因此,有必要显式地<include />包含您想要使用的指令的文件(zope.component中的一个是常见示例)。设置ZCML配置的层可能会公开一个可以作为context参数传递的资源,通常称为configurationContext - 见下文。
要加载特定包的配置,请使用xmlconfig.file():
>>> import zope.component >>> context = xmlconfig.file('meta.zcml', zope.component) >>> xmlconfig.file('configure.zcml', zope.component, context=context) <zope.configuration.config.ConfigurationMachine object at ...>
它需要两个必需的参数:文件名和相对于该模块的文件位置。在这里,我们加载了两个文件:meta.zcml和configure.zcml。第一次调用xmlconfig.file()会创建并返回一个配置上下文。我们重复使用它,以便后续调用中的配置指令可用。
安装 Zope 产品
一些包(包括所有Products.*命名空间中的包)具有特殊的Zope“产品”状态。这些记录在特殊注册表中,并且它们的顶级__init__.py中可能有需要调用的initialize()钩子,以使包完全配置。
Zope 2将在启动时查找和执行任何产品。对于测试,我们需要显式列出要安装的产品。如果您使用Zope与plone.testing,可以使用以下内容:
from plone.testing import zope with zope.zopeApp() as app: zope.installProduct(app, 'Products.ZCatalog')
这通常用于层setUp()期间。请注意,在这样做之前,必须设置基本Zope应用程序上下文。确保这一点的一种常见方法是用基于zope.STARTUP的层 - 见下文。
要拆除此类层,您应该这样做:
from plone.testing import zope with zope.zopeApp() as app: zope.uninstallProduct(app, 'Products.ZCatalog')
注意
与来自ZopeTestCase的类似名称函数不同,这些辅助函数可以与任何类型的产品一起使用。没有“产品”和“包”之间的区别(也没有installPackage())。但是,在注册产品时,必须使用完整的名称(Products.*)。
以这种方式安装产品与ZCML配置无关。但是,通常需要先安装包的ZCML配置。
功能测试
对于旨在模拟浏览器的功能测试,您可以在Python测试或doctest中使用zope.testbrowser:
>>> from zope.testbrowser.browser import Browser >>> browser = Browser()
这提供了一个简单的API来模拟浏览器输入,而不需要实际运行Web服务器线程或脚本化实时浏览器(如Selenium等工具所做的那样)。缺点是,无法测试依赖于JavaScript的行为。
如果您正在测试Zope应用程序,则需要略微更改导入位置,并将应用程序根传递给方法:
from plone.testing.zope import Browser browser = Browser(app)
您可以从该包中此包的任何Zope层中的app资源中获取应用程序根。
除此之外,zope.testbrowser文档应涵盖如何使用测试浏览器。
提示:测试浏览器通常会在请求结束时提交。为了避免测试固定设施的污染,您应该使用完全隔离每个测试的层,例如以下描述的zope.INTEGRATION_TESTING层。
层参考
plone.testing附带了一些可以直接使用或扩展的层。以下概述了这些层。
Zope 组件架构
Zope组件架构层位于模块 plone.testing.zca 中。如果您依赖它,可以在依赖 plone.testing 时使用 [zca] 附加组件。
单元测试
层 |
plone.testing.zca.UNIT_TESTING |
类 |
plone.testing.zca.UnitTesting |
基类 |
None |
资源 |
None |
此层本身不设置固定的测试环境,但在每个测试前后使用 zope.testing.cleanup 清理全局状态,如上所述。
因此,每个测试都有一个干净的全球组件注册表。因此,可以使用 Python API zope.component(provideAdapter()、provideUtility()、provideHandler() 等)来注册组件。
注意将此层与其他层结合使用。因为它会在每个测试之间销毁组件注册表,所以会覆盖任何在组件注册表中设置更永久测试环境的层。
事件测试
层 |
plone.testing.zca.EVENT_TESTING |
类 |
plone.testing.zca.EventTesting |
基类 |
plone.testing.zca.UNIT_TESTING |
资源 |
None |
此层扩展了 zca.UNIT_TESTING 层,以启用来自 zope.component 的 eventtesting 支持。使用此层,您可以导入并使用 zope.component.eventtesting.getEvent 来检查由测试代码触发的事件。
详情见上文。
层清理
层 |
plone.testing.zca.LAYER_CLEANUP |
类 |
plone.testing.zca.LayerCleanup |
基类 |
None |
资源 |
None |
此层在设置和拆除(但不在每个测试之间)时调用 zope.testing.cleanup 中的清理函数。它对于需要尽可能纯净环境的其他层非常有用。
基本 ZCML 指令
层 |
plone.testing.zca.ZCML_DIRECTIVES |
类 |
plone.testing.zca.ZCMLDirectives |
基类 |
plone.testing.zca.LAYER_CLEANUP |
资源 |
configurationContext |
这注册了一组最小的 ZCML 指令,主要是来自 zope.component 包中的指令,并提供了配置上下文。这允许加载自定义 ZCML,如上所述。
当加载自定义 ZCML 时,应使用 configurationContext 资源。为了确保隔离,您应使用 stackConfigurationContext() 辅助程序堆叠它。例如,如果您在一个以 zca.ZCML_DIRECTIVES 为基础的层中编写 setUp() 方法,您可以这样做:
self['configurationContext'] = context = zca.stackConfigurationContext(self.get('configurationContext')) xmlconfig.string(someZCMLString, context=context)
这将创建一个新的配置上下文,其状态与基础层上下文相同。在拆除时,您应该删除层特定资源:
del self['configurationContext']
有关在层或测试中加载自定义 ZCML 的更多详细信息,请参阅上文。
ZCML 文件辅助类
类 |
plone.testing.zca.ZCMLSandbox |
资源 |
configurationContext |
可以使用 filename 和 package 参数实例化 ZCMLSandbox。
ZCML_SANDBOX = zca.ZCMLSandbox(filename="configure.zcml", package=my.package)
该层的 setUp 加载 ZCML 文件。它避免了在需要更多灵活性和模块化时使用(和理解)configurationContext 和 globalRegistry 的需要。
有关在层或测试中加载自定义 ZCML 的更多详细信息,请参阅上文。
辅助函数
在 plone.testing.zca 模块中可用以下辅助函数。
stackConfigurationContext(context=None)
创建并返回传入的 ZCML 配置上下文的副本,或如果它是 None,则创建一个新的上下文。
此功能的目的在于确保如果层在其 setUp() 期间加载一些 ZCML 文件(使用 zope.configuration API),则配置注册表的状态(包括已注册的指令以及已导入的文件列表,即使显式包含也不会再次加载)可以在 tearDown() 期间拆除。
通常的做法是将配置上下文保存在一个名为 configurationContext 的层资源中。在 setUp() 中,你会使用
self['configurationContext'] = context = zca.stackConfigurationContext(self.get('configurationContext')) # use 'context' to load some ZCML在 tearDown() 中,你可以简单地执行
del self['configurationContext']
pushGlobalRegistry(new=None)
创建或获取一个全局组件注册表堆栈,并将一个新的注册表推到堆栈顶部。结果是 zope.component.getGlobalSiteManager() 和(未取消连接的)getSiteManager() 将返回新的注册表而不是默认的模块范围注册表。从这一点开始,对 provideAdapter()、provideUtility() 和其他修改全局注册表功能的调用将使用新的注册表。
如果未提供 new,则创建一个新的注册表,该注册表以前的全局注册表(站点管理器)作为其唯一基础。这导致在以前的默认全局注册表中的注册仍然可用,但新的注册被限制在新的注册表中。
警告:如果您调用此函数,必须相互调用 popGlobalRegistry()。也就是说,如果在层 setUp() 期间“推送”一个注册表,您必须在 tearDown() 中“弹出”它。如果在 testSetUp() 中“推送”,则必须在 testTearDown() 中“弹出”。如果推送和弹出的调用不平衡,您将使全局注册表变得混乱,这是不美观的。
返回新的默认全局站点管理器。还将重置来自 zope.component.hooks 的站点管理器钩子,清除适当的任何本地站点管理器。
popGlobalRegistry()
弹出全局站点注册表,将以前的注册表恢复为默认值。
请注意上面的警告:推送和弹出必须平衡。
返回新的默认全局站点管理器。还将重置来自 zope.component.hooks 的站点管理器钩子,清除适当的任何本地站点管理器。
Zope 安全性
可以在模块 plone.testing.security 中找到 Zope 安全层构建。
如果您依赖此,可以在依赖 plone.testing 时使用 [security] 额外选项。
安全检查器隔离
层 |
plone.testing.security.CHECKERS |
类 |
plone.testing.security.Checkers |
基类 |
None |
资源 |
None |
此层确保 zope.security 使用的安全检查器是隔离的。在子层中设置的任何检查器将在 tearDown() 期间干净地移除。
辅助函数
如上所述的安全检查器隔离是通过模块 plone.testing.security 中找到的两个辅助函数管理的
pushCheckers()
复制当前的设置安全检查器,以便稍后进行拆除。
popCheckers()
将安全检查器集合恢复到最近一次调用 pushCheckers() 时的状态。
必须保持对 pushCheckers() 和 popCheckers() 的调用平衡。这通常意味着,如果您在层设置期间调用前者,则应在层拆除期间调用后者。测试设置/拆除或测试本身中的调用也是如此。
Zope 发布者
在模块 plone.testing.publisher 中可以找到基于 Zope 组件架构层的 Zope 发布者层。
如果您依赖此,可以在依赖 plone.testing 时使用 [publisher] 额外选项。
发布者指令
层 |
plone.testing.publisher.PUBLISHER_DIRECTIVES |
类 |
plone.testing.publisher.PublisherDirectives |
基类 |
plone.testing.zca.ZCML_DIRECTIVES |
资源 |
None |
此层扩展了 zca.ZCML_DIRECTIVES 层,在 browser 命名空间中安装附加的 ZCML 指令(来自 zope.app.publisher.browser)以及来自 zope.security 的那些。这允许注册浏览器视图、浏览器页面和其他 UI 组件,以及定义新权限。
与zca.ZCML_DIRECTIVES类似,在加载ZCML字符串或文件时,您应使用configurationContext资源,并使用stackConfigurationRegistry()辅助函数创建特定于层的此资源版本。请参阅上文。
ZODB
ZODB层设置了一个包含持久化ZODB的测试固定装置。ZODB实例使用DemoStorage,因此它不会干扰任何“实时”数据。
您可以在模块plone.testing.zodb中找到ZODB层。如果您依赖于它,可以在依赖plone.testing时使用[zodb]额外选项。
空 ZODB 沙盒
层 |
plone.testing.zodb.EMPTY_ZODB |
类 |
plone.testing.zodb.EmptyZODB |
基类 |
None |
资源 |
zodbRoot |
zodbDB(仅限测试设置) |
|
zodbConnection(仅限测试设置) |
此层使用DemoStorage设置一个简单的ZODB沙盒。ZODB根对象是一个简单的持久映射,作为资源zodbRoot可用。ZODB数据库对象作为资源zodbDB可用。测试中使用的连接作为zodbConnection可用。
请注意,zodbConnection和zodbRoot资源在每个测试期间创建和销毁。如果您正在编写基于此层的层并需要在层设置期间设置固定装置,您可以使用zodbDB(以及open()方法)。在测试设置完成后,不要忘记关闭连接!
每个测试开始一个新事务,并在测试拆卸时回滚(中止)。这意味着,只要您在代码中未显式使用transaction.commit(),就可以安全地在ZODB根中添加或修改项。
如果您想在基于EMPTY_ZODB的自己的层中创建具有持久数据的测试固定装置,可以使用以下模式
from plone.layer import Layer from plone.layer import zodb class MyLayer(Layer): defaultBases = (zodb.EMPTY_ZODB,) def setUp(self): import transaction self['zodbDB'] = db = zodb.stackDemoStorage(self.get('zodbDB'), name='MyLayer') conn = db.open() root = conn.root() # modify the root object here transaction.commit() conn.close() def tearDown(self): self['zodbDB'].close() del self['zodbDB']
这会使用新的DemoStorage(在底层数据库存储之上堆叠)使用新的数据库来覆盖zodbDB资源。固定装置添加到该存储并在层设置期间提交。(基本层测试设置/拆卸仍将开始和终止每个*测试*的新事务)。在层拆卸时,关闭数据库并弹出资源,留下原始的、未更改的zodbDB数据库。
辅助函数
plone.testing.zodb模块中有一个辅助函数可用。
stackDemoStorage(db=None, name=None)
使用传入数据库的存储创建一个新的DemoStorage。如果db为None,则创建一个全新的存储。
您可以提供一个name来唯一标识存储。这是可选的,但通常在调试时很有用,可以传递层的名称。
通常的模式是
def setUp(self): self['zodbDB'] = zodb.stackDemoStorage(self.get('zodbDB'), name='MyLayer') def tearDown(self): self['zodbDB'].close() del self['zodbDB']
这将使用隔离的DemoStorage覆盖zodbDB资源,如果该资源不存在,则创建一个新实例。所有现有数据仍然可用,但新更改将写入堆叠存储。在拆卸时,关闭堆叠数据库并删除资源,留下原始数据。
Zope
Zope层提供适合测试Zope应用的测试固定装置。它们设置Zope应用程序根,安装核心Zope产品,并管理安全性。
您可以在模块plone.testing.zope中找到Zope层。如果您依赖于它,可以在依赖plone.testing时使用[zope]额外选项。
启动
层 |
plone.testing.zope.STARTUP |
类 |
plone.testing.zope.Startup |
基类 |
plone.testing.zca.LAYER_CLEANUP |
资源 |
zodbDB |
configurationContext |
|
host |
|
port |
该层设置了一个Zope环境,并且是所有其他Zope层的必要基础。由于Zope依赖于一些模块全局状态来运行,而这些状态由该层管理,因此您不能并行运行此层的两个实例。
在设置过程中,该层将配置一个Zope环境,具有以下特点:
启用调试模式。
禁用ZEO客户端缓存。
安装了一些补丁,通过禁用Zope的一些多余方面来加速Zope启动。
一个线程(这仅影响WSGI_SERVER、ZSERVER和FTP_SERVER层)。
使用DemoStorage的原始数据库,作为资源zodbDB公开。Zope配置为使用此数据库,如果使用上述zodb.EMPTY_ZODB层描述中显示的模式使用zodbDB资源进行阴影,则也可以使用此数据库。
一个伪造的主机名和端口号,分别作为资源host和port公开。
安装了最小的一组产品(Products.OFSP和Products.PluginIndexes,这两者都是Zope启动所必需的)。
一个堆叠的ZCML配置上下文,作为资源configurationContext公开。如图所示,如果您使用此配置,应使用zca.stackConfigurationContext()辅助工具堆叠自己的配置上下文。
配置了最小的一组全局Zope组件。
请注意,与“真实”的Zope站点不同,Products.*命名空间中的产品不会自动加载,也不会加载任何ZCML。
集成测试
层 |
plone.testing.zope.INTEGRATION_TESTING |
类 |
plone.testing.zope.IntegrationTesting |
基类 |
plone.testing.zope.STARTUP |
资源 |
app |
request |
此层旨在针对简单的STARTUP设置进行集成测试。如果您想创建自己的层,并具有更高级的共享设置,请参阅下文“使用自定义设置进行集成和功能测试”。
对于每个测试,它将Zope应用程序根作为资源app公开。这被封装在请求容器中,因此您可以使用app.REQUEST获取一个假请求,但请求也作为资源request可用。
对于每个测试,都会开始一个新的事务,并在测试拆卸时回滚,这意味着只要测试中的代码没有明确提交任何更改,测试就可以修改ZODB。
提示:如果您想在基于此层(或zope.FUNCTIONAL_TESTING)的层中设置持久测试设置,您可以在阴影的zodbDB资源中使用新的DemoStorage,使用上述zodb.EMPTY_ZODB层描述中所述的模式。
一旦阴影了zodbDB资源,您就可以这样做(例如,在您的层的setUp()方法中):
... with zope.zopeApp() as app: # modify the Zope application rootzopeApp()上下文管理器将打开一个新连接到Zope应用程序根,在这里作为app可访问。只要with块内的代码没有抛出异常,事务将在退出块时提交,并正确关闭数据库。
功能测试
层 |
plone.testing.zope.FUNCTIONAL_TESTING |
类 |
plone.testing.zope.FunctionalTesting |
基类 |
plone.testing.zope.STARTUP |
资源 |
app |
request |
此层旨在针对简单的STARTUP夹具进行功能测试。如果您想创建一个具有更高级、共享夹具的自定义层,请参阅下文的“使用自定义夹具进行集成和功能测试”。
正如其名称所暗示的,此层主要用于使用诸如zope.testbrowser之类的工具进行功能端到端测试。有关“辅助函数”下所述的Browser对象,也请参阅。
此层与INTEGRATION_TESTING非常相似,但不是基于它的。它设置相同的夹具并公开相同的资源。然而,它不是使用简单的事务中止来隔离测试之间的ZODB,而是为每个测试使用堆叠的DemoStorage。这速度较慢,但允许测试代码执行显式的提交,这在功能测试中通常会发生。
使用自定义固定值的集成和功能测试
如果您想将STARTUP夹具扩展以用于集成或功能测试,您应使用以下模式
创建一个层类和一个具有zope.STARTUP(或某些中间层,如下面所示的zope.WSGI_SERVER_FIXTURE)作为基的“夹具”基础层实例。
通过以这个新的“夹具”层为基来实例化zope.IntegrationTesting和/或FunctionalTesting类来创建“最终用户”层。
这允许无论测试的“风格”如何,都可以使用相同的夹具,最小化设置和拆除的工作量。“夹具”层将夹具作为层生命周期的一部分来管理。层类(IntegrationTesting或FunctionalTesting)管理测试生命周期,并且仅管理测试生命周期。
例如
from plone.testing import Layer, zope, zodb class MyLayer(Layer): defaultBases = (zope.STARTUP,) def setUp(self): # Set up the fixture here ... def tearDown(self): # Tear down the fixture here ... MY_FIXTURE = MyLayer() MY_INTEGRATION_TESTING = zope.IntegrationTesting(bases=(MY_FIXTURE,), name="MyFixture:Integration") MY_FUNCTIONAL_TESTING = zope.FunctionalTesting(bases=(MY_FIXTURE,), name="MyFixture:Functional")
(注意,我们需要为重用IntegrationTesting和FunctionalTesting类的两个层提供一个明确、唯一的名称。)
在此示例中,其他层可以通过使用MY_FIXTURE作为基来扩展“MyLayer”夹具。测试将根据需要使用MY_INTEGRATION_TESTING或MY_FUNCTIONAL_TESTING。然而,即使这两个层都被使用,MY_FIXTURE中的夹具也只会设置一次。
因此,最好拥有自己的“测试生命周期”层类,这些类是IntegrationTesting和/或FunctionalTesting的子类,并根据需要调用基类方法。plone.app.testing就是采用这种方法的一个例子。
HTTP WSGI 服务器线程(仅限固定值)
层 |
plone.testing.zope.WSGI_SERVER_FIXTURE |
类 |
plone.testing.zope.WSGIServer |
基类 |
plone.testing.zope.STARTUP |
资源 |
host |
port |
此层扩展了zope.STARTUP层,以在单独的线程中启动Zope HTTP WSGI服务器。这意味着测试站点可以通过网络浏览器访问,因此可以与诸如Selenium之类的工具一起使用。
WSGI服务器的域名(通常为localhost)可通过资源host获取,而它运行的端口可通过资源port获取。
提示: 在层设置过程中,您实际上可以通过网页浏览器访问测试 Zope 网站。默认 URL 为 https://127.0.0.1:55001。
HTTP WSGI 服务器功能测试
层 |
plone.testing.zope.WSGI_SERVER |
类 |
plone.testing.zope.FunctionalTesting |
基类 |
plone.testing.zope.WSGI_SERVER_FIXTURE |
资源 |
该层提供了对由 zope.WSGI_SERVER_FIXTURE 层设置的固定设置的函数测试生命周期。
您可以使用此功能对基本 Zope 网站运行“实时”功能测试。您不应将其用作基础。相反,创建自己的“固定”层,该层扩展 zope.WSGI_SERVER_FIXTURE,然后使用上述方法以该扩展的固定层为基础实例化 FunctionalTesting 类。
辅助函数
在 plone.testing.zope 模块中提供了几个辅助函数。
zopeApp(db=None, conn=Non, environ=None)
此函数可以用作任何需要访问 Zope 应用程序根的代码的上下文管理器。通过将其放入 with 块中,将打开数据库,并获取应用程序根并对其进行请求包装。退出 with 块时,将提交事务并正确关闭数据库,除非抛出异常。
with zope.zopeApp() as app: # do something with app如果您想使用特定的数据库或数据库连接,请传递 db 或 conn 参数。如果上下文管理器打开了新的连接,它将关闭它,但它不会关闭通过 conn 传递的连接。
要将键设置在(虚假的)请求环境中,请将环境值字典作为 environ 传递。
请注意,通常不应在测试或测试设置/拆除中使用 zopeApp(),因为 INTEGRATOIN_TEST 和 FUNCTIONAL_TESTING 层都管理应用程序根(作为 app 资源)并为您关闭它。然而,它在层设置中非常有用。
installProduct(app, product, quiet=False)
安装 Zope 2 风格的产品,确保其 initialize() 函数被调用。产品名称必须是完整的点名称,例如 plone.app.portlets 或 Products.CMFCore。如果 quiet 为 true,则静默忽略重复注册,否则记录一条消息。
要获取作为 app 参数传递的应用程序根,您通常会使用上面概述的 zopeApp() 上下文管理器。
uninstallProduct(app, product, quiet=False)
这是 installProduct() 的相反操作,通常在层拆除期间使用。同样,您应该使用 zopeApp() 来获取应用程序根。
login(userFolder, userName)
创建一个新的安全管理器,模拟以给定用户登录。 userFolder 是一个 acl_users 对象,例如根用户文件夹的 app['acl_users']。
logout()
通过取消设置安全管理器来模拟匿名用户。
setRoles(userFolder, userName, roles)
将给定用户在给定用户文件夹中的角色设置为给定角色列表。
makeTestRequest()
创建一个虚假的 Zope 请求。
addRequestContainer(app, environ=None)
创建一个虚假请求并将给定对象(通常为应用程序根)包装在一个具有此请求的 RequestContainer 中。这使得获取 app.REQUEST 成为可能。为了使用非默认值初始化请求环境,请传递一个字典作为 environ。
Browser(app)
获取一个用于与zope.testbrowser一起使用的测试浏览器客户端。您应该与此结合使用zope.FUNCTIONAL_TESTING层或其衍生层。您必须传递应用程序根,通常从层的app资源中获取,例如。
app = self.layer['app'] browser = zope.Browser(app)然后您可以像在zope.testbrowser文档中所描述的那样使用browser。
请注意,测试浏览器在测试固定装置之外单独运行。特别是,对login()或logout()等辅助程序的调用不会影响测试浏览器看到的状态。如果您想设置一个持久固定装置(例如测试内容),您可以在创建测试浏览器之前这样做,但您需要显式提交您的更改,使用
import transaction transaction.commit()
ZServer
ZServer层提供了适合使用ZServer而不是WSGI服务器测试Zope应用程序的测试固定装置。它们设置Zope应用程序根,安装核心Zope产品,并管理安全。
您可以在模块plone.testing.zserver中找到ZServer层。如果您依赖于它,您可以在依赖plone.testing时使用[zope,zserver]额外。
启动
层 |
plone.testing.zserver.STARTUP |
类 |
plone.testing.zserver.Startup |
基类 |
plone.testing.zca.LAYER_CLEANUP |
资源 |
zodbDB |
configurationContext |
|
host |
|
port |
此层为ZServer设置Zope环境,是所有其他ZServer层的必要基础。您不能并行运行此层的两个实例,因为Zope依赖于一些模块全局状态来运行,这些状态由该层管理。
在设置时,该层将配置与zope.Startup相同的选项的Zope环境,请参阅那里。
集成测试
层 |
plone.testing.zserver.INTEGRATION_TESTING |
类 |
plone.testing.zserver.IntegrationTesting |
基类 |
plone.testing.zserver.STARTUP |
资源 |
app |
request |
此层旨在针对简单的STARTUP设置进行集成测试。如果您想创建自己的层,并具有更高级的共享设置,请参阅下文“使用自定义设置进行集成和功能测试”。
对于每个测试,它将Zope应用程序根作为资源app公开。这被封装在请求容器中,因此您可以使用app.REQUEST获取一个假请求,但请求也作为资源request可用。
对于每个测试,都会开始一个新的事务,并在测试拆卸时回滚,这意味着只要测试中的代码没有明确提交任何更改,测试就可以修改ZODB。
提示:如果您想在基于此(或zserver.FUNCTIONAL_TESTING)的层中设置一个持久的测试固定装置,您可以在一个阴影的zodbDB资源中使用上面描述的zodb.EMPTY_ZODB层的模式,在阴影中堆叠一个新的DemoStorage。
一旦阴影了zodbDB资源,您就可以这样做(例如,在您的层的setUp()方法中):
... with zserver.zopeApp() as app: # modify the Zope application rootzserver.zopeApp()上下文管理器将打开到Zope应用程序根的新连接,在此处作为app可用。只要with块内的代码没有引发异常,事务将在退出块时提交,数据库将正确关闭。
功能测试
层 |
plone.testing.zserver.FUNCTIONAL_TESTING |
类 |
plone.testing.zserver.FunctionalTesting |
基类 |
plone.testing.zserver.STARTUP |
资源 |
app |
request |
它的行为与zope.FunctionalTesting相同,请参阅那里。
使用自定义固定值的集成和功能测试
如果您想将STARTUP夹具扩展以用于集成或功能测试,您应使用以下模式
创建一个层类和一个具有zserver.STARTUP(或某些中间层,例如下面的zserver.ZSERVER_FIXTURE或zserver.FTP_SERVER_FIXTURE)作为基础的“固定装置”层实例。
通过将这个新的“固定装置”层作为基础来创建“最终用户”层,通过实例化zserver.IntegrationTesting和/或FunctionalTesting类。
这允许无论测试的“风格”如何,都可以使用相同的夹具,最小化设置和拆除的工作量。“夹具”层将夹具作为层生命周期的一部分来管理。层类(IntegrationTesting或FunctionalTesting)管理测试生命周期,并且仅管理测试生命周期。
例如
from plone.testing import Layer, zserver, zodb class MyLayer(Layer): defaultBases = (zserver.STARTUP,) def setUp(self): # Set up the fixture here ... def tearDown(self): # Tear down the fixture here ... MY_FIXTURE = MyLayer() MY_INTEGRATION_TESTING = zserver.IntegrationTesting(bases=(MY_FIXTURE,), name="MyFixture:Integration") MY_FUNCTIONAL_TESTING = zserver.FunctionalTesting(bases=(MY_FIXTURE,), name="MyFixture:Functional")
(注意,我们需要为重用IntegrationTesting和FunctionalTesting类的两个层提供一个明确、唯一的名称。)
在此示例中,其他层可以通过使用MY_FIXTURE作为基来扩展“MyLayer”夹具。测试将根据需要使用MY_INTEGRATION_TESTING或MY_FUNCTIONAL_TESTING。然而,即使这两个层都被使用,MY_FIXTURE中的夹具也只会设置一次。
因此,最好拥有自己的“测试生命周期”层类,这些类是IntegrationTesting和/或FunctionalTesting的子类,并根据需要调用基类方法。plone.app.testing就是采用这种方法的一个例子。
HTTP ZServer 线程(仅限固定值)
层 |
plone.testing.zserver.ZSERVER_FIXTURE |
类 |
plone.testing.zserver.ZServer |
基类 |
plone.testing.zserver.STARTUP |
资源 |
host |
port |
此层扩展了zserver.STARTUP层,在单独的线程中启动Zope HTTP服务器(ZServer)。这意味着测试站点可以通过网络浏览器访问,因此可以与Selenium等工具一起使用。
ZServer的主机名(通常是 localhost)可以通过资源 host 获取,而它运行的端口可以通过资源 port 获取。
提示: 在层设置过程中,您实际上可以通过网页浏览器访问测试 Zope 网站。默认 URL 为 https://127.0.0.1:55001。
HTTP ZServer 功能测试
层 |
plone.testing.zserver.ZSERVER |
类 |
plone.testing.zserver.FunctionalTesting |
基类 |
plone.testing.zserver.ZSERVER_FIXTURE |
资源 |
该层提供了针对由 zserver.ZSERVER_FIXTURE 层设置的夹具的功能测试生命周期。
您可以使用此功能在基本 Zope 网站上运行“实时”功能测试。您不应该将其用作基础。相反,创建自己的“夹具”层,该层扩展 zserver.ZSERVER_FIXTURE,然后使用上面概述的方式将这个扩展的夹具层作为基础实例化 FunctionalTesting 类。
FTP 服务器线程(仅限固定值)
层 |
plone.testing.zserver.FTP_SERVER_FIXTURE |
类 |
plone.testing.zserver.FTPServer |
基类 |
plone.testing.zserver.STARTUP |
资源 |
host |
port |
该层是 zserver.ZSERVER_FIXTURE 层的 FTP 服务器等效层。它可以用来对 Zope FTP 服务器进行功能测试。
如果您需要同时运行 ZServer 和 FTPServer,可以继承 ZServer 层类(就像 FTPServer 层类所做的那样)并实现 setUpServer() 和 tearDownServer() 方法来在不同的端口上设置和关闭两个服务器。然后它们将共享一个主循环。
FTP 服务器功能测试
层 |
plone.testing.zserver.FTP_SERVER |
类 |
plone.testing.zserver.FunctionalTesting |
基类 |
plone.testing.zserver.FTP_SERVER_FIXTURE |
资源 |
该层提供了针对由 zserver.FTP_SERVER_FIXTURE 层设置的夹具的功能测试生命周期。
您可以使用此功能在基本 Zope 网站上运行“实时”功能测试。您不应该将其用作基础。相反,创建自己的“夹具”层,该层扩展 zserver.FTP_SERVER_FIXTURE,然后使用上面概述的方式将这个扩展的夹具层作为基础实例化 FunctionalTesting 类。
辅助函数
plone.testing.zserver 模块中提供了几个辅助函数。
zopeApp(db=None, conn=Non, environ=None)
此函数可以用作任何需要访问 Zope 应用程序根的代码的上下文管理器。通过将其放入 with 块中,将打开数据库,并获取应用程序根并对其进行请求包装。退出 with 块时,将提交事务并正确关闭数据库,除非抛出异常。
with zserver.zopeApp() as app: # do something with app如果您想使用特定的数据库或数据库连接,请传递 db 或 conn 参数。如果上下文管理器打开了新的连接,它将关闭它,但它不会关闭通过 conn 传递的连接。
要将键设置在(虚假的)请求环境中,请将环境值字典作为 environ 传递。
请注意,通常不应在测试或测试设置/拆除中使用 zopeApp(),因为 INTEGRATOIN_TEST 和 FUNCTIONAL_TESTING 层都管理应用程序根(作为 app 资源)并为您关闭它。然而,它在层设置中非常有用。
在 plone.testing.zope 中定义的其他辅助函数也可以在 ZServer 上下文中使用,但与 ZServer 层一起使用。
变更日志
9.0.2 (2024-06-26)
错误修复
makeTestRequest:使用 BytesIO 设置测试响应。@gotcha(fix_makeTestRequest)
9.0.1 (2023-11-30)
错误修复
移除对 five.localsitemanager 的不正确硬依赖。@davisagli(#86)
9.0.0 (2023-10-25)
破坏性更改
取消对 python 2.7 的支持。 [gforcada](#1)
取消 ZServer 支持。 [gforcada](#2)
内部
更新配置文件。 [plone 开发者](5cc689e5)
8.0.4 (2023-09-21)
错误修复
修复运行 ZODB 5.8.1+ 时的测试。 [maurits](#581)
8.0.3 (2021-06-14)
错误修复
修复 waitress 弃用警告 (#77)
在测试结束时捕获 OSError,当删除临时目录时。修复 问题 79。[maurits](#79)
8.0.2 (2020-10-12)
错误修复
更新 isort 配置以适应 isort 版本 5。 (#75)
8.0.1 (2020-06-16)
错误修复
修复损坏的 Flake8 作业 (#74)
8.0.0 (2020-04-21)
破坏性更改
取消对 Python 3.4 和 3.5 的支持。删除“z2”额外功能。[jensens](#72)
新功能
更新有关 测试 的更多信息链接。[jugmac00](#71)
错误修复
修复使用 zope.testrunner 内部版本 5.1 进行测试时的测试设置问题。[jensens](#72)
7.0.3 (2019-12-10)
错误修复
修复使用 ZServer 4.0.2 时的测试设置问题。[pbauer](#69)
7.0.2 (2019-07-06)
错误修复
移除 ZOPETESTCASEALERT,因为它从 ZopeTestCase 导入并存在副作用。修复 #64。[thet] (#67)
7.0.1 (2019-03-03)
错误修复
修复了对“连接被拒绝”的测试,这可能是“连接被重置”。[maurits] (#59)
7.0.0 (2018-10-17)
破坏性更改
plone.testing.z2 现在是 plone.testing.zope 的 BBB 模拟器,因此它将测试切换到使用 WSGI。如果您绝对想继续使用 ZServer,请从 plone.testing.zserver 导入。
plone.testing.z2 现在只包含一个 no-op FTPServer 层,因为 WSGI 不支持 FTP。如果您确实需要它,请从 plone.testing.zserver 导入,但这在 Python 3 上将不起作用。
默认为 ZServer 层选择动态端口,而不是静态默认端口。[Rotonen]
新功能
将 ZServer 设为可选依赖项。
增加对 Python 3.6 的支持。[rudaporto, icemac]
错误修复
在 z2 extra 上显式依赖 ZServer。[Rotonen]
6.1.0 (2018-10-05)
破坏性更改
默认为 ZServer 层选择动态端口,而不是静态默认端口。[Rotonen]
错误修复
将 ZODB 锁定在 < 5.4.0,以便于测试,避免 doctest 层拆解不稳定。[Rotonen]
放宽 doctest 断言以跟上 Zope 方面的变化。[Rotonen]
修复 Jenkins 抱怨的大部分代码味道。
修复使用 ZServer 层时的 Zope 异常钩子。
修复 plone.testing.security.Checkers 层的拆解。它没有正确地恢复 zope.security 的 _checkers 字典。
6.0.0 (2018-02-05)
破坏性更改
仅支持 Zope >= 4,不再支持 Zope2。
放弃对 Python 2.6 的支持。
不再使用 getSite/setSite 的弃用导入。[jensens]
更新代码以遵循 Plone 风格指南。[gforcada]
5.1.1 (2017-04-19)
当使用 zope.testbrowser >= 5.0 时,不要在导入 plone.testing.z2 时中断,因为该版本不再依赖于 mechanize。[do3cc]
5.1 (2017-04-13)
为 ZODB 5 修复:在数据库关闭前中止事务。[jensens, jimfulton]
移除 Zope < 2.13 的 BBB 代码和导入。[thet]
修复在 Python 3 上使用 layered-helper 时的问题。[datakurre]
修复 .z2.Startup.setUpZCML(),使其与 Zope >= 4.0a2 兼容。[icemac]
修复包本身的版本锁定,以便能够运行测试。[gforcada]
5.0.0 (2016-02-19)
重新发布 4.2.0 为 5.0.0。
版本 4.2.0 修改了公共 api 中的错误处理,导致以前一直正常工作的异常。[icemac]
4.2.0 (2016-02-18)
新增
如果用户破坏测试隔离,则拒绝工作。[do3cc]
检查测试是否与 ZopeTestCase 一起运行。[do3cc]
修复
修复 Zope 4 的测试,其中应用根 Control_Panel 已不再可用。[thet]
4.1.0 (2016-01-08)
修复
将所有 txt doctest 文件重命名为 rst。重排 doctests。[thet]
PEP 8。[thet]
依赖 zope.testrunner,它已从 zope.testing.testrunner 中移出。[thet]
增加对 Zope 4 的支持。[thet]
4.0.15 (2015-08-14)
防止在 zopeApp 上下文的 finally 子句中屏蔽异常。[do3cc]
4.0.14 (2015-07-29)
由于 4.0.13 重复发布,重新发布以增加清晰度。[maurits]
向 z2.installProduct 添加 multiinit 参数,以便为包提供多个初始化方法。[tomgross]
4.0.13 (2015-03-13)
真正修复不依赖 unittest2。[icemac]
添加 tox.ini。[icemac]
4.0.12 (2014-09-07)
修复在 zope.testbrowser 4.x 被使用但未使用 zope.app.testing 时导入 plone.testing.z2 的 AttributeError。[icemac]
由于 unittest2 的所有功能都已集成到 unittest 中,因此取消对 Python 2.7+ 的 unittest2 的依赖。[icemac]
4.0.11 (2014-02-22)
修复 z2.txt doctest 的 FTP_SERVER。[timo]
4.0.10 (2014-02-11)
如果可能,从环境变量中读取 FTPSERVER_HOST 和 FTPSERVER_PORT。这允许我们在 CI 服务器上并行运行测试。[timo]
4.0.9 (2014-01-28)
替换弃用的 Zope2VocabularyRegistry 导入。[timo]
4.0.8 (2013-03-05)
将测试请求的创建从 addRequestContainer 因素化到 makeTestRequest。[davisagli]
4.0.7 (2012-12-09)
修复 testbrowser 对 url 的引号处理。[do3cc]
4.0.6 (2012-10-15)
更新 manifest.in,包括 src 目录中的内容。[esteele]
4.0.5 (2012-10-15)
修复了查询字符串被重复两次未加引号的问题;一次是在设置HTTP请求时,一次是在处理程序(发布者)中。[malthe]
4.0.4 (2012-08-04)
修复了缓存重置代码。在某些情况下,该函数没有任何默认值,因此我们不应尝试清除应用程序引用。[malthe]
4.0.3 (2011-11-24)
修复了文档中的类名,使其与代码匹配。[icemac]
4.0.2 (2011-08-31)
ZPublisher.Publish.get_module_info函数的默认值缓存了应用程序的引用,因此在拆卸应用程序时确保其被重置。这解决了测试浏览器在第二个要设置的功能层中从第一个功能层访问数据库的问题。[davisagli]
4.0.1 - 2011-05-20
将包含测试的readme文件移至包中,以便可以从发布源分布运行测试。解决了http://dev.plone.org/plone/ticket/11821。[hannosch]
在BSD许可下重新授权。请参阅http://plone.org/foundation/materials/foundation-resolutions/plone-framework-components-relicensing-policy。[davisagli]
4.0 - 2011-05-13
发布4.0最终版。[esteele]
添加了MANIFEST.in。[WouterVH]
4.0a6 - 2011-04-06
修复了使用Zope 2.13检索浏览器cookie的问题。[vincentfretin]
将ZCMLSandbox层添加到加载ZCML文件;替代了setUpZcmlFiles和tearDownZcmlFiles辅助函数。[gotcha]
4.0a5 - 2011-03-02
处理由于Zope较新版本中的userFolderAddUser返回用户对象而导致的测试失败。[esteele]
添加了setUpZcmlFiles和tearDownZcmlFiles辅助函数,以便在不添加太多样板代码的情况下加载ZCML文件。[gotcha]
添加了一些日志。[gotcha]
添加了[security]额外内容,以提供安全检查器的拆卸。[optilude]
让IntegrationTesting和FunctionalTesting生命周期层设置请求PARENTS,如果存在,则连接zope.globalrequest。[optilude]
使测试浏览器支持IStreamIterators。[optilude]
4.0a4 - 2011-01-11
确保在Zope 2.13中应用程序启动期间不加载ZCML。[davisagli]
4.0a3 - 2010-12-14
如果存在,忽略测试home配置设置。[stefan]
使用Zope 2.13中获取packages_to_initialize列表的新API。[davisagli]
在Zope 2.13中启动层的拆卸过程中,在正确模块中删除_duplicate_monkies和_duplicate_type_regs。[davisagli]
允许从zope.testing的doctest套件与plone.testing.layer.layered一起工作。以前,只有来自stdlib的doctest套件会看到layer全局。[nouri]
更改文档以宣传使用coverage库运行覆盖率测试,而不是使用内置的zope.testing支持。这也避免了使用z3c.coverage。现在覆盖率测试与正常测试运行的速度相同,使其更有可能频繁执行。[hannosch]
正确许可为仅GPL版本2。[hannosch]
修复了一些用户ID与名称的混淆。[rossp]
添加通过环境变量指定ZServer主机和端口的选项 - ZSERVER_HOST和ZSERVER_PORT。[esteele]
1.0a2 - 2010-09-05
修复了一个可能导致<meta:redefinePermission />中断的问题。特别是修复了使用zope2.Public权限。[optilude]
将安全实现设置为“Python”,以便在z2.STARTUP层期间更容易调试。[optilude]
在z2.Startup层中初始化Five,在层设置时推入Zope2VocabularyRegistry,并在拆卸时恢复先前的一个。[dukebody]
1.0a1 - 2010-08-01
首次发布
详细文档
层基类
此包提供了一个层基类,可以被测试运行器使用。它作为从包根的便利导入可用。
>>> from plone.testing import Layer
层可以直接实例化,尽管在这种情况下需要name参数(见下文)。
>>> NULL_LAYER = Layer(name="Null layer")
这本身并不是非常有用。它有一个空的基类列表,并且每个层生命周期方法都不执行任何操作。
>>> NULL_LAYER.__bases__ () >>> NULL_LAYER.__name__ 'Null layer' >>> NULL_LAYER.__module__ 'plone.testing.layer' >>> NULL_LAYER.setUp() >>> NULL_LAYER.testSetUp() >>> NULL_LAYER.tearDown() >>> NULL_LAYER.testTearDown()
直接使用这个组件(即不作为基类)的主要原因是为了将其他层组合在一起。
>>> SIMPLE_LAYER = Layer(bases=(NULL_LAYER,), name="Simple layer", module='plone.testing.tests')
在这里,我们还直接设置了模块名称。对于所有层,默认情况下都是从实例化层的栈帧中获取模块名称。但在doctests中,这不起作用,所以我们回退到层类的模块名称。当然,这两个名称通常是一样的。
这个层现在具有我们设置的基数、名称和模块:
>>> SIMPLE_LAYER.__bases__ (<Layer 'plone.testing.layer.Null layer'>,) >>> SIMPLE_LAYER.__name__ 'Simple layer' >>> SIMPLE_LAYER.__module__ 'plone.testing.tests'
当直接使用Layer时(但使用子类时则不是),必须提供name参数:
>>> Layer((SIMPLE_LAYER,)) Traceback (most recent call last): ... ValueError: The `name` argument is required when instantiating `Layer` directly >>> class NullLayer(Layer): ... pass >>> NullLayer() <Layer 'builtins.NullLayer'>
将 Layer 作为基类使用
通常的做法是将Layer用作自定义层的基类。然后可以适当地覆盖生命周期方法,以及设置默认的基数列表。
>>> class BaseLayer(Layer): ... ... def setUp(self): ... print("Setting up base layer") ... ... def tearDown(self): ... print("Tearing down base layer") >>> BASE_LAYER = BaseLayer()
层名称和模块来自类。
>>> BASE_LAYER.__bases__ () >>> BASE_LAYER.__name__ 'BaseLayer' >>> BASE_LAYER.__module__ 'builtins'
现在我们可以创建一个新的层,将其作为基类。我们可以在实例构造函数中这样做,如上所示,但最常见的模式是在类体中使用变量defaultBases设置默认基数。
我们还将通过传递一个名称给父构造函数来显式设置默认名称。这主要是为了外观,但如果类名在测试运行器输出中具有误导性,可能是有用的。
>>> class ChildLayer(Layer): ... defaultBases = (BASE_LAYER,) ... ... def __init__(self, bases=None, name='Child layer', module=None): ... super(ChildLayer, self).__init__(bases, name, module) ... ... def setUp(self): ... print("Setting up child layer") ... ... def tearDown(self): ... print("Tearing down child layer") >>> CHILD_LAYER = ChildLayer()
注意,现在基数是使用defaultBases中的值设置的。
>>> CHILD_LAYER.__bases__ (<Layer 'builtins.BaseLayer'>,) >>> CHILD_LAYER.__name__ 'Child layer' >>> CHILD_LAYER.__module__ 'builtins'
覆盖默认基础层列表
我们可以在每个实例的基础上覆盖基数列表。这可能很危险,即层可能会期望其基数已设置。然而,有时注入新的基数可能很有用,尤其是在重用来自其他包的层时。
新的基数列表传递给构造函数。在创建层的第二个实例(大多数层是全局单例,仅创建一次)时,给新实例提供一个独特的名称也很有用。
>>> NEW_CHILD_LAYER = ChildLayer(bases=(SIMPLE_LAYER, BASE_LAYER,), name='New child') >>> NEW_CHILD_LAYER.__bases__ (<Layer 'plone.testing.tests.Simple layer'>, <Layer 'builtins.BaseLayer'>) >>> NEW_CHILD_LAYER.__name__ 'New child' >>> NEW_CHILD_LAYER.__module__ 'builtins'
不一致的基础层
层基数按照与Python维护基类的“方法解析顺序”语义等效的顺序维护。我们可以从baseResolutionOrder属性中获取它:
>>> CHILD_LAYER.baseResolutionOrder (<Layer 'builtins.Child layer'>, <Layer 'builtins.BaseLayer'>) >>> NEW_CHILD_LAYER.baseResolutionOrder (<Layer 'builtins.New child'>, <Layer 'plone.testing.tests.Simple layer'>, <Layer 'plone.testing.layer.Null layer'>, <Layer 'builtins.BaseLayer'>)
与Python类一样,可能构造一个无效的基数集。在这种情况下,层实例化将失败。
>>> INCONSISTENT_BASE1 = Layer(name="Inconsistent 1") >>> INCONSISTENT_BASE2 = Layer((INCONSISTENT_BASE1,), name="Inconsistent 1") >>> INCONSISTENT_BASE3 = Layer((INCONSISTENT_BASE1, INCONSISTENT_BASE2,), name="Inconsistent 1") Traceback (most recent call last): ... TypeError: Inconsistent layer hierarchy!
使用资源管理器
层也是资源管理器。可以使用字典语法设置、检索和删除资源。基层中的资源在子层中可用。当在子层上设置一个项时,它会覆盖任何具有相同键的任何基层中的项(直到它被删除),但原始项仍然存在。
让我们创建一个相当复杂的层层次结构,其中所有层都在它们的setUp()方法中设置一个键'foo'下的资源。
>>> class Layer1(Layer): ... def setUp(self): ... self['foo'] = 1 ... def tearDown(self): ... del self['foo'] >>> LAYER1 = Layer1() >>> class Layer2(Layer): ... defaultBases = (LAYER1,) ... def setUp(self): ... self['foo'] = 2 ... def tearDown(self): ... del self['foo'] >>> LAYER2 = Layer2() >>> class Layer3(Layer): ... def setUp(self): ... self['foo'] = 3 ... def tearDown(self): ... del self['foo'] >>> LAYER3 = Layer3() >>> class Layer4(Layer): ... defaultBases = (LAYER2, LAYER3,) ... def setUp(self): ... self['foo'] = 4 ... def tearDown(self): ... del self['foo'] >>> LAYER4 = Layer4() **Important:** Resources that are created in ``setUp()`` must be deleted in ``tearDown()``. Similarly, resources created in ``testSetUp()`` must be deleted in ``testTearDown()``. This ensures resources are properly stacked and do not leak between layers.
如果测试使用LAYER4,测试运行器将按顺序调用每个设置步骤,从“最深”层开始。我们将在这里模拟这一点,以便创建每个资源。
>>> LAYER1.setUp() >>> LAYER2.setUp() >>> LAYER3.setUp() >>> LAYER4.setUp()
层按照已知的“资源解析顺序”排序,该顺序用于确定层以何种顺序相互覆盖。这基于与Python的方法解析顺序相同的算法。
>>> LAYER4.baseResolutionOrder (<Layer 'builtins.Layer4'>, <Layer 'builtins.Layer2'>, <Layer 'builtins.Layer1'>, <Layer 'builtins.Layer3'>)
从层中获取项时,将根据资源解析顺序获取。
>>> LAYER4['foo'] 4
这并不特别有趣,因为LAYER4直接设置了资源'foo'。让我们拆毁层(删除资源)并看看会发生什么。
>>> LAYER4.tearDown() >>> LAYER4['foo'] 2
我们可以继续向上链:
>>> LAYER2.tearDown() >>> LAYER4['foo'] 1 >>> LAYER1.tearDown() >>> LAYER4['foo'] 3
一旦我们删除了最后一个键,我们将得到一个KeyError:
>>> LAYER3.tearDown() >>> LAYER4['foo'] Traceback (most recent call last): ... KeyError: 'foo'
为了防止这种情况,我们可以使用get()方法。
>>> LAYER4.get('foo', -1) -1
我们也可以使用in进行测试:
>>> 'foo' in LAYER4 False
为了说明这确实有效,让我们将资源设置回其中一个基类。
>>> LAYER3['foo'] = 10 >>> LAYER4.get('foo', -1) 10
现在让我们考虑一个特殊情况:基础层在层设置中设置了一个资源,并在测试设置中使用它。子层随后在其自己的层设置方法中覆盖了这个资源。在这种情况下,我们希望基础层的testSetUp()使用子层提供的覆盖版本。
(这与实例变量的工作方式类似:基类可能在self上设置一个属性并使用它。如果一个子类随后将该属性设置为不同的值,并且在子类的实例上调用基类的方法,则使用基类属性)。
提示:如果您确实需要在您的testSetUp()/testTearDown()方法中访问“原始”资源,您可以将资源引用存储为层实例变量
self.someResource = self['someResource'] = SomeResource()self.someResource现在将是此处创建的确切资源,而self['someResource']将保留层覆盖的语义。在大多数情况下,您可能不想这样做,允许子层根据需要提供资源覆盖的版本。
首先,我们将创建一些基础层。我们想展示有两个“分支”的基础层,它们恰好定义了相同的资源。
>>> class ResourceBaseLayer1(Layer): ... def setUp(self): ... self['resource'] = "Base 1" ... def testSetUp(self): ... print(self['resource']) ... def tearDown(self): ... del self['resource'] >>> RESOURCE_BASE_LAYER1 = ResourceBaseLayer1() >>> class ResourceBaseLayer2(Layer): ... defaultBases = (RESOURCE_BASE_LAYER1,) ... def testSetUp(self): ... print(self['resource']) >>> RESOURCE_BASE_LAYER2 = ResourceBaseLayer2() >>> class ResourceBaseLayer3(Layer): ... def setUp(self): ... self['resource'] = "Base 3" ... def testSetUp(self): ... print(self['resource']) ... def tearDown(self): ... del self['resource'] >>> RESOURCE_BASE_LAYER3 = ResourceBaseLayer3()
然后我们将创建一个覆盖这个资源的子层。
>>> class ResourceChildLayer(Layer): ... defaultBases = (RESOURCE_BASE_LAYER2, RESOURCE_BASE_LAYER3) ... def setUp(self): ... self['resource'] = "Child" ... def testSetUp(self): ... print(self['resource']) ... def tearDown(self): ... del self['resource'] >>> RESOURCE_CHILD_LAYER = ResourceChildLayer()
我们将首先单独设置基础层,并模拟两个测试。
只有一个RESOURCE_BASE_LAYER1的测试看起来像这样:
>>> RESOURCE_BASE_LAYER1.setUp() >>> RESOURCE_BASE_LAYER1.testSetUp() Base 1 >>> RESOURCE_BASE_LAYER1.testTearDown() >>> RESOURCE_BASE_LAYER1.tearDown()
有一个RESOURCE_BASE_LAYER2的测试看起来像这样:
>>> RESOURCE_BASE_LAYER1.setUp() >>> RESOURCE_BASE_LAYER2.setUp() >>> RESOURCE_BASE_LAYER1.testSetUp() Base 1 >>> RESOURCE_BASE_LAYER2.testSetUp() Base 1 >>> RESOURCE_BASE_LAYER2.testTearDown() >>> RESOURCE_BASE_LAYER1.testTearDown() >>> RESOURCE_BASE_LAYER2.tearDown() >>> RESOURCE_BASE_LAYER1.tearDown()
只有一个RESOURCE_BASE_LAYER3的测试看起来像这样:
>>> RESOURCE_BASE_LAYER3.setUp() >>> RESOURCE_BASE_LAYER3.testSetUp() Base 3 >>> RESOURCE_BASE_LAYER3.testTearDown() >>> RESOURCE_BASE_LAYER3.tearDown()
现在让我们设置子层并模拟另一个测试。我们现在应该使用覆盖后的资源。
>>> RESOURCE_BASE_LAYER1.setUp() >>> RESOURCE_BASE_LAYER2.setUp() >>> RESOURCE_BASE_LAYER3.setUp() >>> RESOURCE_CHILD_LAYER.setUp() >>> RESOURCE_BASE_LAYER1.testSetUp() Child >>> RESOURCE_BASE_LAYER2.testSetUp() Child >>> RESOURCE_BASE_LAYER3.testSetUp() Child >>> RESOURCE_CHILD_LAYER.testSetUp() Child >>> RESOURCE_CHILD_LAYER.testTearDown() >>> RESOURCE_BASE_LAYER3.testTearDown() >>> RESOURCE_BASE_LAYER2.testTearDown() >>> RESOURCE_BASE_LAYER1.testTearDown()
最后,我们将再次拆除子层并模拟另一个测试。我们应该回到原始资源。注意,第一和第三层不再共享资源,因为它们没有共同的祖先。
>>> RESOURCE_CHILD_LAYER.tearDown() >>> RESOURCE_BASE_LAYER1.testSetUp() Base 1 >>> RESOURCE_BASE_LAYER2.testSetUp() Base 1 >>> RESOURCE_BASE_LAYER2.testTearDown() >>> RESOURCE_BASE_LAYER1.testTearDown() >>> RESOURCE_BASE_LAYER3.testSetUp() Base 3 >>> RESOURCE_BASE_LAYER3.testTearDown()
最后,我们将拆除剩余的层。
>>> RESOURCE_BASE_LAYER3.tearDown() >>> RESOURCE_BASE_LAYER2.tearDown() >>> RESOURCE_BASE_LAYER1.tearDown()
非对称删除
在设置生命周期方法中创建或覆盖资源,然后在拆除时没有再次删除,这是一个错误。删除未明确创建的资源也是一个错误。这两个层打破了这些角色:
>>> class BadLayer1(Layer): ... def setUp(self): ... pass ... def tearDown(self): ... del self['foo'] >>> BAD_LAYER1 = BadLayer1() >>> class BadLayer2(Layer): ... defaultBases = (BAD_LAYER1,) ... def setUp(self): ... self['foo'] = 1 ... self['bar'] = 2 >>> BAD_LAYER2 = BadLayer2()
让我们模拟一个使用BAD_LAYER2的测试:
>>> BAD_LAYER1.setUp() >>> BAD_LAYER2.setUp() >>> BAD_LAYER1.testSetUp() >>> BAD_LAYER2.testSetUp() >>> BAD_LAYER2.testTearDown() >>> BAD_LAYER1.testTearDown() >>> BAD_LAYER2.tearDown() >>> BAD_LAYER1.tearDown() Traceback (most recent call last): ... KeyError: 'foo'
这里,我们在基类中有一个错误。这是因为资源实际上与首次创建它的层相关联,在这种情况下是BASE_LAYER2。这个层仍然完好无损且被遗弃:
>>> 'foo' in BAD_LAYER2._resources True >>> 'bar' in BAD_LAYER2._resources True
Doctest 层辅助
doctest模块不了解zope.testing的层概念。因此,创建带有层并将其添加到测试套件的语法有些复杂:测试套件必须首先创建,然后在该套件上设置层属性:
>>> class DoctestLayer(Layer): ... pass >>> DOCTEST_LAYER = DoctestLayer() >>> import unittest >>> import doctest >>> def test_suite(): ... suite = unittest.TestSuite() ... layerDoctest = doctest.DocFileSuite('layer.rst', package='plone.testing') ... layerDoctest.layer = DOCTEST_LAYER ... suite.addTest(layerDoctest) ... return suite >>> suite = test_suite() >>> tests = list(suite) >>> len(tests) 1 >>> tests[0].layer is DOCTEST_LAYER True
为了使其更简单一些——特别是当设置多个测试时——提供了一个名为layered的辅助函数:
>>> from plone.testing import layered >>> def test_suite(): ... suite = unittest.TestSuite() ... suite.addTests([ ... layered(doctest.DocFileSuite('layer.rst', package='plone.testing'), layer=DOCTEST_LAYER), ... # repeat with more suites if necessary ... ]) ... return suite
这个函数与上面的示例做的是同样的事情。
>>> suite = test_suite() >>> tests = list(suite) >>> len(tests) 1 >>> tests[0].layer is DOCTEST_LAYER True
此外,将“层”通配符添加到套件中的每个测试中。这允许测试访问层资源。
>>> len(list(tests[0])) 1 >>> list(tests[0])[0]._dt_test.globs['layer'] is DOCTEST_LAYER True
Zope 组件架构层
ZCA层位于模块plone.testing.zca中:
>>> from plone.testing import zca
对于测试,我们需要一个测试运行器:
>>> from zope.testrunner import runner
单元测试
UNIT_TESTING层用于在每个测试之间设置一个干净的组件注册表。它使用zope.testing.cleanup来清理所有全局状态。
它没有基类:
>>> "%s.%s" % (zca.UNIT_TESTING.__module__, zca.UNIT_TESTING.__name__,) 'plone.testing.zca.UnitTesting' >>> zca.UNIT_TESTING.__bases__ ()
组件注册表在每次测试之间清理。
>>> from zope.interface import Interface >>> from zope.component import provideUtility >>> class DummyUtility(object): ... def __init__(self, name): ... self.name = name ... def __repr__(self): ... return "<%s>" % self.name >>> provideUtility(DummyUtility("Dummy"), provides=Interface, name="test-dummy") >>> from zope.component import queryUtility >>> queryUtility(Interface, name="test-dummy") <Dummy>
层设置什么都不做。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zca.UNIT_TESTING, setupLayers) Set up plone.testing.zca.UnitTesting in ... seconds.
现在让我们模拟一个测试。在任何测试设置发生之前,我们之前注册的实用工具仍然存在。
>>> queryUtility(Interface, name="test-dummy") <Dummy>
在测试设置时,它消失了。
>>> zca.UNIT_TESTING.testSetUp() >>> queryUtility(Interface, name="test-dummy") is None True
测试现在将执行。它可能注册了一些组件。
>>> provideUtility(DummyUtility("Dummy2"), provides=Interface, name="test-dummy") >>> queryUtility(Interface, name="test-dummy") <Dummy2>
在测试拆除时,这消失了。
>>> zca.UNIT_TESTING.testTearDown() >>> queryUtility(Interface, name="test-dummy") is None True
层拆除什么都不做。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zca.UnitTesting in ... seconds.
事件测试
《EVENT_TESTING》层扩展了《UNIT_TESTING》层,以便添加必要的注册,使《zope.component.eventtesting》能够工作。
>>> "%s.%s" % (zca.EVENT_TESTING.__module__, zca.EVENT_TESTING.__name__,) 'plone.testing.zca.EventTesting' >>> zca.EVENT_TESTING.__bases__ (<Layer 'plone.testing.zca.UnitTesting'>,)
在测试之前,组件注册表为空,即使触发事件,getEvents()也返回空。
>>> from zope.component.eventtesting import getEvents >>> class DummyEvent(object): ... def __repr__(self): ... return "<Dummy event>" >>> from zope.event import notify >>> notify(DummyEvent()) >>> getEvents() []
层设置什么都不做。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zca.EVENT_TESTING, setupLayers) Set up plone.testing.zca.UnitTesting in ... seconds. Set up plone.testing.zca.EventTesting in ... seconds.
现在我们来模拟一个测试。在测试设置时,事件测试列表被清空。
>>> zca.UNIT_TESTING.testSetUp() >>> zca.EVENT_TESTING.testSetUp() >>> getEvents() []
测试现在将执行。它可能会触发一些事件,这些事件将在事件测试列表中显示。
>>> notify(DummyEvent()) >>> getEvents() [<Dummy event>]
在测试清理时,列表再次被清空:
>>> zca.EVENT_TESTING.testTearDown() >>> zca.UNIT_TESTING.testTearDown() >>> getEvents() []
层拆除什么都不做。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zca.EventTesting in ... seconds. Tear down plone.testing.zca.UnitTesting in ... seconds.
层清理
《LAYER_CLEANUP》层用于在层的设置和清理时设置干净的组件注册表。它使用《zope.testing.cleanup》来清理所有全局状态。
它没有基类:
>>> "%s.%s" % (zca.LAYER_CLEANUP.__module__, zca.LAYER_CLEANUP.__name__,) 'plone.testing.zca.LayerCleanup' >>> zca.LAYER_CLEANUP.__bases__ ()
在层设置和清理时清理组件注册表(但不是在测试之间)。
>>> from zope.interface import Interface >>> from zope.component import provideUtility >>> class DummyUtility(object): ... def __init__(self, name): ... self.name = name ... def __repr__(self): ... return "<%s>" % self.name >>> provideUtility(DummyUtility("Dummy"), provides=Interface, name="test-dummy") >>> from zope.component import queryUtility >>> queryUtility(Interface, name="test-dummy") <Dummy> >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zca.LAYER_CLEANUP, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. >>> queryUtility(Interface, name="test-dummy") is None True
子层可以注册额外的组件:
>>> provideUtility(DummyUtility("Dummy2"), provides=Interface, name="test-dummy2")
现在我们来模拟一个测试。测试设置和清理不执行任何操作。
>>> zca.LAYER_CLEANUP.testSetUp() >>> queryUtility(Interface, name="test-dummy") is None True >>> queryUtility(Interface, name="test-dummy2") <Dummy2> >>> zca.LAYER_CLEANUP.testTearDown() >>> queryUtility(Interface, name="test-dummy") is None True >>> queryUtility(Interface, name="test-dummy2") <Dummy2>
在清理时,注册表再次被清理。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zca.LayerCleanup in ... seconds. >>> queryUtility(Interface, name="test-dummy") is None True >>> queryUtility(Interface, name="test-dummy2") is None True
基本 ZCML 指令
《ZCML_DIRECTIVES》层创建一个ZCML配置上下文,其中包含基本的《zope.component》指令。它扩展了《LAYER_CLEANUP》层。
>>> "%s.%s" % (zca.ZCML_DIRECTIVES.__module__, zca.ZCML_DIRECTIVES.__name__,) 'plone.testing.zca.ZCMLDirectives' >>> zca.ZCML_DIRECTIVES.__bases__ (<Layer 'plone.testing.zca.LayerCleanup'>,)
在测试之前,我们无法使用例如《 层设置创建了一个配置上下文,我们可以用它来加载更多的配置。 现在我们来模拟一个测试,该测试使用这个配置上下文来加载相同的ZCML字符串。 实用工具现在已被注册: 请注意,通常我们会将此与《UNIT_TESTING》层结合使用,以便在清理组件架构的同时进行清理。 层清理删除配置上下文。>>> from zope.configuration import xmlconfig
>>> from zope.configuration.exceptions import ConfigurationError
>>> try:
... xmlconfig.string("""\
... <configure package="plone.testing" xmlns="http://namespaces.zope.org/zope">
... <utility factory=".tests.DummyUtility" provides="zope.interface.Interface" name="test-dummy" />
... </configure>""")
... except ConfigurationError as e:
... True
True
>>> options = runner.get_options([], [])
>>> setupLayers = {}
>>> runner.setup_layer(options, zca.ZCML_DIRECTIVES, setupLayers)
Set up plone.testing.zca.LayerCleanup in ... seconds.
Set up plone.testing.zca.ZCMLDirectives in ... seconds.
>>> zca.ZCML_DIRECTIVES.testSetUp()
>>> context = zca.ZCML_DIRECTIVES['configurationContext'] # would normally be self.layer['configurationContext']
>>> xmlconfig.string("""\
... <configure package="plone.testing" xmlns="http://namespaces.zope.org/zope">
... <utility factory=".tests.DummyUtility" provides="zope.interface.Interface" name="test-dummy" />
... </configure>""", context=context) is context
True
>>> queryUtility(Interface, name="test-dummy")
<Dummy utility>
>>> zca.UNIT_TESTING.testTearDown()
>>> runner.tear_down_unneeded(options, [], setupLayers, [])
Tear down plone.testing.zca.ZCMLDirectives in ... seconds.
>>> zca.ZCML_DIRECTIVES.get('configurationContext', None) is None
True
配置注册沙盒
对于简单的单元测试,使用《UNIT_TESTING》层在每次测试之间执行的完整清理无疑是确保使用全局组件架构进行测试适当隔离的最安全和最方便的方式。然而,如果您正在编写一个设置大量组件的复杂层,您可能希望在该层级别保留一些组件注册,同时仍然允许测试和子层独立注册自己的组件。
这是一个棘手的问题,因为默认的ZCML指令和API(《provideAdapter()》、《provideUtility()》等)明确地在单个全局适配器注册表对象上工作。为了解决这个问题,您可以在《zca》模块中使用两个辅助方法,在注册组件之前推入新的全局组件注册表,并在注册后弹出注册表。注册表是堆叠的,因此注册在“较低”注册表中的组件在“较高”注册表中自动可用。
让我们通过一个堆叠两个新全局注册表的层来说明这一点。第一个注册表是针对层的特定注册表,用于存放在该层级别注册的组件。第二个注册表为每个测试设置和清理,允许测试自由注册自己的组件。
首先,我们将创建一个简单的模拟实用工具来展示注册。
>>> from zope.interface import Interface, implementer >>> class IDummyUtility(Interface): ... pass >>> @implementer(IDummyUtility) ... class DummyUtility(object): ... def __init__(self, name): ... self.name = name ... def __repr__(self): ... return "<DummyUtility %s>" % self.name
两个关键方法是
zca.pushGlobalRegistry(),它创建一个新的全局注册表。
zca.popGlobalRegistry(),它恢复先前的全局注册表。
警告:您必须平衡对这些方法的调用。如果在《setUp()》中调用《pushGlobalRegistry()`》,则在《tearDown()`中调用《popGlobalRegistry()`》。同样适用于《testSetUp()`》和《testTearDown()`》。
现在让我们创建我们的层。
>>> from zope.component import provideUtility >>> from plone.testing import Layer >>> from plone.testing import zca >>> class ComponentSandbox(Layer): ... def setUp(self): ... zca.pushGlobalRegistry() ... provideUtility(DummyUtility("layer"), name="layer") ... def tearDown(self): ... zca.popGlobalRegistry() ... def testSetUp(self): ... zca.pushGlobalRegistry() ... def testTearDown(self): ... zca.popGlobalRegistry() >>> COMPONENT_SANDBOX = ComponentSandbox()
现在让我们模拟使用这个层的测试。
起初,我们有默认的注册表。
>>> from zope.component import getGlobalSiteManager, getSiteManager >>> getSiteManager() is getGlobalSiteManager() True >>> defaultGlobalSiteManager = getGlobalSiteManager() >>> from zope.component import queryUtility >>> queryUtility(IDummyUtility, name="layer") is None True
我们现在将模拟层设置。这将把一个新的注册表压入堆栈:
>>> COMPONENT_SANDBOX.setUp() >>> getSiteManager() is getGlobalSiteManager() True >>> getGlobalSiteManager() is defaultGlobalSiteManager False >>> layerGlobalSiteManager = getGlobalSiteManager() >>> queryUtility(IDummyUtility, name="layer") <DummyUtility layer>
然后我们将模拟一个注册全局组件的测试:
>>> COMPONENT_SANDBOX.testSetUp() >>> getSiteManager() is getGlobalSiteManager() True >>> getGlobalSiteManager() is defaultGlobalSiteManager False >>> getGlobalSiteManager() is layerGlobalSiteManager False
我们之前注册的组件仍然在这里。
>>> queryUtility(IDummyUtility, name="layer") <DummyUtility layer>
我们也可以注册一个新的。
>>> provideUtility(DummyUtility("test"), name="test") >>> queryUtility(IDummyUtility, name="layer") <DummyUtility layer> >>> queryUtility(IDummyUtility, name="test") <DummyUtility test>
在测试拆除时,只有第二个实用工具消失:
>>> COMPONENT_SANDBOX.testTearDown() >>> getSiteManager() is getGlobalSiteManager() True >>> getGlobalSiteManager() is defaultGlobalSiteManager False >>> getGlobalSiteManager() is layerGlobalSiteManager True >>> queryUtility(IDummyUtility, name="layer") <DummyUtility layer> >>> queryUtility(IDummyUtility, name="test") is None True
如果我们也拆除层,我们就会回到起点:
>>> COMPONENT_SANDBOX.tearDown() >>> getSiteManager() is getGlobalSiteManager() True >>> getGlobalSiteManager() is defaultGlobalSiteManager True >>> queryUtility(IDummyUtility, name="layer") is None True >>> queryUtility(IDummyUtility, name="test") is None True
ZCML 文件辅助类
其中一个常见用例是一个加载ZCML文件并在沙盒中封装结果的层。
可以使用`filename`和`package`参数来实例化`ZCMLSandbox`。
>>> import plone.testing >>> ZCML_SANDBOX = zca.ZCMLSandbox(filename="testing_zca.zcml", ... package=plone.testing)
在层设置之前,实用工具未注册。
>>> queryUtility(Interface, name="layer") is None True
我们现在将模拟层设置。这将把一个新的注册表压入堆栈:
>>> ZCML_SANDBOX.setUp() >>> getSiteManager() is getGlobalSiteManager() True >>> getGlobalSiteManager() is defaultGlobalSiteManager False >>> queryUtility(Interface, name="layer") <Dummy utility>
当需要加载多个ZCML文件时,`ZCMLSandbox`类也可以用作您自己类的前身。
然后您的类需要覆盖`setUpZCMLFiles()`方法。它负责对每个需要加载的ZCML文件调用一次`loadZCMLFile()`。
>>> class OtherZCML(zca.ZCMLSandbox): ... def setUpZCMLFiles(self): ... self.loadZCMLFile("testing_zca.zcml", package=plone.testing) ... self.loadZCMLFile("testing_zca_more_specific.zcml", ... package=plone.testing) >>> OTHER_ZCML_SANDBOX = OtherZCML()
在层设置之前,第二个实用工具未注册。
>>> queryUtility(Interface, name="more_specific_layer") is None True
我们现在将模拟更具体层的设置。
>>> OTHER_ZCML_SANDBOX.setUp()
在setUp之后,第二个实用工具已注册:
>>> queryUtility(Interface, name="more_specific_layer") <Dummy utility>
在层拆除后,第二个实用工具不再注册。
>>> OTHER_ZCML_SANDBOX.tearDown() >>> queryUtility(Interface, name="more_specific_layer") is None True
在第一层拆除后,第一个实用工具不再注册。
>>> ZCML_SANDBOX.tearDown() >>> queryUtility(Interface, name="layer") is None True
安全性
Zope安全层位于`plone.testing.security`模块中:
>>> from plone.testing import security
对于测试,我们需要一个测试运行器:
>>> from zope.testrunner import runner
层
`security.CHECKERS`层确保`zope.security`检查器被正确设置和拆除。
>>> "%s.%s" % (security.CHECKERS.__module__, security.CHECKERS.__name__,) 'plone.testing.security.Checkers' >>> security.CHECKERS.__bases__ ()
在测试之前,我们的自定义检查器不在注册表中。
>>> class DummyObject(object): ... pass >>> from zope.security.interfaces import IChecker >>> from zope.interface import implementer >>> @implementer(IChecker) ... class FauxChecker(object): ... # we should really implement the interface here, but oh well ... pass >>> from zope.security.checker import getCheckerForInstancesOf >>> getCheckerForInstancesOf(DummyObject) is None True
层设置堆栈当前检查器。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, security.CHECKERS, setupLayers) Set up plone.testing.security.Checkers in ... seconds.
现在我们可以设置一个检查器。在现实生活中,这可能会发生在ZCML配置期间,但在这里我们将直接调用API。很可能会在子层中这样做:
>>> from zope.security.checker import defineChecker >>> fauxChecker = FauxChecker() >>> defineChecker(DummyObject, fauxChecker) >>> getCheckerForInstancesOf(DummyObject) is fauxChecker True
现在让我们模拟一个可能使用检查器的测试。
>>> security.CHECKERS.testSetUp() >>> getCheckerForInstancesOf(DummyObject) is fauxChecker True >>> security.CHECKERS.testTearDown()
在测试拆除后,我们仍然有检查器:
>>> getCheckerForInstancesOf(DummyObject) is fauxChecker True
然而,当我们拆除层时,检查器消失了:
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.security.Checkers in ... seconds. >>> getCheckerForInstancesOf(DummyObject) is None True
Zope 发布者层
Zope发布层位于`plone.testing.publisher`模块中:
>>> from plone.testing import publisher
对于测试,我们需要一个测试运行器:
>>> from zope.testrunner import runner
ZCML 指令
`publisher.PUBLISHER_DIRECTIVES`层扩展了`zca.ZCML_DIRECTIVES`层,将其ZCML配置上下文扩展到包含`zope.app.publisher`和`zope.security`指令。它还扩展了`security.CHECKERS`。
>>> from plone.testing import zca, security >>> "%s.%s" % (publisher.PUBLISHER_DIRECTIVES.__module__, publisher.PUBLISHER_DIRECTIVES.__name__,) 'plone.testing.publisher.PublisherDirectives' >>> publisher.PUBLISHER_DIRECTIVES.__bases__ (<Layer 'plone.testing.zca.ZCMLDirectives'>, <Layer 'plone.testing.security.Checkers'>)
在测试之前,我们不能使用例如`
>>> from zope.configuration import xmlconfig >>> from zope.configuration.exceptions import ConfigurationError >>> try: ... xmlconfig.string("""\ ... <configure package="plone.testing" ... xmlns="http://namespaces.zope.org/zope" ... xmlns:browser="http://namespaces.zope.org/browser" ... i18n_domain="plone.testing.tests"> ... <permission id="plone.testing.Test" title="plone.testing: Test" /> ... <browser:view ... for="*" ... name="plone.testing-test" ... class="plone.testing.tests.DummyView" ... permission="zope.Public" ... /> ... </configure>""") ... except ConfigurationError as e: ... True True
层设置创建了一个配置上下文,我们可以用它来加载更多的配置。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, publisher.PUBLISHER_DIRECTIVES, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zca.ZCMLDirectives in ... seconds. Set up plone.testing.security.Checkers in ... seconds. Set up plone.testing.publisher.PublisherDirectives in ... seconds.
现在我们来模拟一个测试,该测试使用这个配置上下文来加载相同的ZCML字符串。
>>> zca.ZCML_DIRECTIVES.testSetUp() >>> security.CHECKERS.testSetUp() >>> publisher.PUBLISHER_DIRECTIVES.testSetUp() >>> context = zca.ZCML_DIRECTIVES['configurationContext'] # would normally be self.layer['configurationContext'] >>> xmlconfig.string("""\ ... <configure package="plone.testing" ... xmlns="http://namespaces.zope.org/zope" ... xmlns:browser="http://namespaces.zope.org/browser" ... i18n_domain="plone.testing.tests"> ... <permission id="plone.testing.Test" title="plone.testing: Test" /> ... <browser:view ... for="*" ... name="plone.testing-test" ... class="plone.testing.tests.DummyView" ... permission="zope.Public" ... /> ... </configure>""", context=context) is context True
权限和视图现在已注册:
>>> from zope.component import queryUtility >>> from zope.security.interfaces import IPermission >>> queryUtility(IPermission, name=u"plone.testing.Test") <zope.security.permission.Permission object at ...> >>> from zope.interface import Interface >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer >>> from zope.component import getSiteManager >>> siteManager = getSiteManager() >>> [x.factory for x in siteManager.registeredAdapters() ... if x.provided==Interface and x.required==(Interface, IDefaultBrowserLayer) ... and x.name==u"plone.testing-test"] [<class '....plone.testing-test'>]
然后我们可以模拟测试拆除:
>>> publisher.PUBLISHER_DIRECTIVES.testTearDown() >>> security.CHECKERS.testTearDown() >>> zca.ZCML_DIRECTIVES.testTearDown()
请注意,您通常会将此层与`zca.UNIT_TESTING`或类似的层结合使用,以在每次测试之间自动拆除组件架构。在这里,我们需要手动执行。
>>> from zope.component.testing import tearDown >>> tearDown()
层拆除什么都不做。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.publisher.PublisherDirectives in ... seconds. Tear down plone.testing.zca.ZCMLDirectives in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds. Tear down plone.testing.security.Checkers in ... seconds. >>> zca.ZCML_DIRECTIVES.get('configurationContext', None) is None True
Zope 对象数据库层
ZODB层位于`plone.testing.zodb`模块中:
>>> from plone.testing import zodb
对于测试,我们需要一个测试运行器:
>>> from zope.testrunner import runner
空 ZODB 层
`EMPTY_ZODB`层用于使用`DemoStorage`设置一个空的ZODB。
存储和数据库作为层固定装置设置。数据库作为资源`zodbDB`公开。
为每个测试打开一个连接,并将其作为`zodbConnection`公开。ZODB根也作为`zodbRoot`公开。每个测试开始一个新的事务。在测试拆除时,事务被中止,连接被关闭,并删除两个特定于测试的资源。
层没有基。
>>> "%s.%s" % (zodb.EMPTY_ZODB.__module__, zodb.EMPTY_ZODB.__name__,) 'plone.testing.zodb.EmptyZODB' >>> zodb.EMPTY_ZODB.__bases__ ()
层设置创建数据库,但不建立连接。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zodb.EMPTY_ZODB, setupLayers) Set up plone.testing.zodb.EmptyZODB in ... seconds. >>> db = zodb.EMPTY_ZODB['zodbDB'] >>> db.storage EmptyZODB >>> zodb.EMPTY_ZODB.get('zodbConnection', None) is None True >>> zodb.EMPTY_ZODB.get('zodbRoot', None) is None True
现在让我们模拟一个测试。
>>> zodb.EMPTY_ZODB.testSetUp()
测试将执行。它可能会使用ZODB根目录。
>>> zodb.EMPTY_ZODB['zodbConnection'] <...Connection...at ...> >>> zodb.EMPTY_ZODB['zodbRoot'] {} >>> zodb.EMPTY_ZODB['zodbRoot']['foo'] = 'bar'
在测试拆除时,事务被终止,连接被关闭。
>>> zodb.EMPTY_ZODB.testTearDown() >>> zodb.EMPTY_ZODB.get('zodbConnection', None) is None True >>> zodb.EMPTY_ZODB.get('zodbRoot', None) is None True
事务已被回滚。
>>> conn = zodb.EMPTY_ZODB['zodbDB'].open() >>> conn.root() {} >>> conn.close()
层拆除关闭并删除数据库。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zodb.EmptyZODB in ... seconds. >>> zodb.EMPTY_ZODB.get('zodbDB', None) is None True
扩展 ZODB 层
在创建测试固定件时,通常希望向数据库中添加一些初始数据。如果您想在层设置时一次性完成,可以创建基于EmptyZODB的自定义层类,并覆盖其createStorage()和/或createDatabase()方法以返回预填充的数据库。
>>> import transaction >>> from ZODB.DemoStorage import DemoStorage >>> from ZODB.DB import DB >>> class PopulatedZODB(zodb.EmptyZODB): ... ... def createStorage(self): ... return DemoStorage("My storage") ... ... def createDatabase(self, storage): ... db = DB(storage) ... conn = db.open() ... ... conn.root()['someData'] = 'a string' ... ... transaction.commit() ... conn.close() ... ... return db >>> POPULATED_ZODB = PopulatedZODB()
我们将以类似于上面测试的方式使用这个新的层,显示数据在每个测试中都是存在的,但其他更改被回滚。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, POPULATED_ZODB, setupLayers) Set up ...PopulatedZODB in ... seconds. >>> db = POPULATED_ZODB['zodbDB'] >>> db.storage My storage >>> POPULATED_ZODB.get('zodbConnection', None) is None True >>> POPULATED_ZODB.get('zodbRoot', None) is None True
现在让我们模拟一个测试。
>>> POPULATED_ZODB.testSetUp()
测试将执行。它可能会使用ZODB根目录。
>>> POPULATED_ZODB['zodbConnection'] <...Connection...at ...> >>> POPULATED_ZODB['zodbRoot'] {'someData': 'a string'} >>> POPULATED_ZODB['zodbRoot']['foo'] = 'bar'
在测试拆除时,事务被终止,连接被关闭。
>>> POPULATED_ZODB.testTearDown() >>> POPULATED_ZODB.get('zodbConnection', None) is None True >>> POPULATED_ZODB.get('zodbRoot', None) is None True
事务已被回滚。
>>> conn = POPULATED_ZODB['zodbDB'].open() >>> conn.root() {'someData': 'a string'} >>> conn.close()
层拆除关闭并删除数据库。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down ...PopulatedZODB in ... seconds. >>> POPULATED_ZODB.get('zodbDB', None) is None True
堆叠 DemoStorage 存储器
上面的例子展示了如何创建一个带有自定义数据库的简单测试固定件。有时能够堆叠这些固定件很有用,这样基础层为一系列测试设置一些数据,子层临时地扩展更多数据。
这可以通过使用层基和资源阴影以及ZODB的可堆叠DemoStorage来实现。甚至还有一个辅助函数可用:
>>> from plone.testing import Layer >>> from plone.testing import zodb >>> import transaction >>> class ExpandedZODB(Layer): ... defaultBases = (POPULATED_ZODB,) ... ... def setUp(self): ... # Get the database from the base layer ... ... self['zodbDB'] = db = zodb.stackDemoStorage(self.get('zodbDB'), name='ExpandedZODB') ... ... conn = db.open() ... conn.root()['additionalData'] = "Some new data" ... transaction.commit() ... conn.close() ... ... def tearDown(self): ... # Close the database and delete the shadowed copy ... ... self['zodbDB'].close() ... del self['zodbDB'] >>> EXPANDED_ZODB = ExpandedZODB()
请注意,我们在这里使用的是普通的Layer作为基类。我们使用资源管理器从基类获取底层数据库,然后使用堆叠存储创建一个阴影副本。堆叠存储包含原始存储的数据,但将更改保存在一个单独的(在这个情况下是临时的)存储中。
让我们再次模拟一个测试运行来展示这是如何工作的。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, EXPANDED_ZODB, setupLayers) Set up ...PopulatedZODB in ... seconds. Set up ...ExpandedZODB in ... seconds. >>> db = EXPANDED_ZODB['zodbDB'] >>> db.storage ExpandedZODB >>> EXPANDED_ZODB.get('zodbConnection', None) is None True >>> EXPANDED_ZODB.get('zodbRoot', None) is None True
现在让我们模拟一个测试。
>>> POPULATED_ZODB.testSetUp() >>> EXPANDED_ZODB.testSetUp()
测试将执行。它可能会使用ZODB根目录。
>>> EXPANDED_ZODB['zodbConnection'] <...Connection...at ...> >>> EXPANDED_ZODB['zodbRoot'] == dict(someData='a string', additionalData='Some new data') True >>> POPULATED_ZODB['zodbRoot']['foo'] = 'bar'
在测试拆除时,事务被终止,连接被关闭。
>>> EXPANDED_ZODB.testTearDown() >>> POPULATED_ZODB.testTearDown() >>> EXPANDED_ZODB.get('zodbConnection', None) is None True >>> EXPANDED_ZODB.get('zodbRoot', None) is None True
事务已被回滚。
>>> conn = EXPANDED_ZODB['zodbDB'].open() >>> conn.root() == dict(someData='a string', additionalData='Some new data') True >>> conn.close()
现在我们将拆除扩展的层,并再次检查数据库。
>>> runner.tear_down_unneeded(options, [POPULATED_ZODB], setupLayers, []) Tear down ...ExpandedZODB in ... seconds. >>> conn = EXPANDED_ZODB['zodbDB'].open() >>> conn.root() {'someData': 'a string'} >>> conn.close()
最后,我们将拆除其他层。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down ...PopulatedZODB in ... seconds. >>> EXPANDED_ZODB.get('zodbDB', None) is None True >>> POPULATED_ZODB.get('zodbDB', None) is None True
Zope WSGI 层
Zope WSGI层位于模块plone.testing.zope中:
>>> from plone.testing import zope
对于测试,我们需要一个测试运行器:
>>> from zope.testrunner import runner
启动
STARTUP是所有Zope WSGI测试的基础层。它设置一个适合测试的Zope WSGI沙盒环境,并扩展zca.LAYER_CLEANUP层,以最大化拥有并留下一个干净环境的机会。
注意:您可能至少应该使用INTEGRATION_TESTING来进行任何实际测试,尽管如果您正在设置自己的固定件,STARTUP是一个有用的基础层。请参阅下面关于INTEGRATION_TESTING的描述。
>>> "%s.%s" % (zope.STARTUP.__module__, zope.STARTUP.__name__,) 'plone.testing.zope.Startup' >>> zope.STARTUP.__bases__ (<Layer 'plone.testing.zca.LayerCleanup'>,)
层设置时,Zope以轻量级的方式初始化。这包括对Zope管理的全局模块的某些修补,以减少设置时间,一个基于DemoStorage的数据库,以及Zope 2必须安装的一组最小产品。加载了一组最小的ZCML,但Products命名空间中的包不会自动配置。
让我们在测试之前验证我们有一个空的组件注册表:
>>> from zope.component import getSiteManager >>> list(getSiteManager().registeredAdapters()) []
Five在层设置时设置了一个特殊的词汇注册表,但在设置之前有一个默认的设置:
>>> from zope.schema.vocabulary import getVocabularyRegistry >>> getVocabularyRegistry() <zope.schema.vocabulary.VocabularyRegistry object ...> >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zope.STARTUP, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zope.Startup in ... seconds.
层设置后,zodbDB资源可用,指向默认的ZODB。
>>> zope.STARTUP['zodbDB'] <ZODB.DB.DB object at ...> >>> zope.STARTUP['zodbDB'].storage Startup
此外,资源host和port被设置为用于从Zope生成URL的默认主机名和端口号。这些是硬编码的,但被提供实际运行Zope实例的层所阴影。
>>> zope.STARTUP['host'] 'nohost' >>> zope.STARTUP['port'] 80
在此阶段,还可能获取一个Zope应用程序根。如果您正在设置层固定件,可以使用正确的数据库并通过使用zopeApp()上下文管理器来获取一个正确关闭的应用程序根。
>>> with zope.zopeApp() as app: ... 'acl_users' in app.objectIds() True
如果您想使用特定的数据库,可以将它传递给 zopeApp() 函数作为 db 参数。将打开并关闭一个新连接。
>>> with zope.zopeApp(db=zope.STARTUP['zodbDB']) as app: ... 'acl_users' in app.objectIds() True
如果您想重用现有连接,可以将它传递给 zopeApp() 函数作为 connection 参数。在这种情况下,您需要自己关闭连接。
>>> conn = zope.STARTUP['zodbDB'].open() >>> with zope.zopeApp(connection=conn) as app: ... 'acl_users' in app.objectIds() True >>> conn.opened is not None True >>> conn.close()
如果在 with 块中抛出异常,事务将被中止,但连接仍然会被关闭(如果它是由上下文管理器打开的):
>>> with zope.zopeApp() as app: ... raise Exception("Test error") Traceback (most recent call last): ... Exception: Test error
通常将 zopeApp() 上下文管理器与堆叠的 DemoStorage 结合使用,以设置特定层的固定值。如下所示:
from plone.testing import Layer, zope, zodb class MyLayer(Layer): defaultBases = (zope.STARTUP,) def setUp(self): self['zodbDB'] = zodb.stackDemoStorage(self.get('zodbDB'), name='MyLayer') with zope.zopeApp() as app: # Set up a fixture, e.g.: app.manage_addFolder('folder1') folder = app['folder1'] folder._addRole('role1') folder.manage_addUserFolder() userFolder = folder['acl_users'] ignore = userFolder.userFolderAddUser('user1', 'secret', ['role1'], []) folder.manage_role('role1', ('Access contents information',)) def tearDown(self): self['zodbDB'].close() del self['zodbDB']
请注意,您通常 不会 在测试或 testSetUp() 或 testTearDown() 方法中使用 zope.zopeApp()。IntegrationTesting 和 FunctionalTesting 层类为您管理应用程序对象,并将它们作为资源 app 暴露出来(见下文)。
层设置后,全局组件注册表中包含 Zope 需要的许多组件。
>>> len(list(getSiteManager().registeredAdapters())) > 1 # in fact, > a lot True
并且 Five 设置了一个 Zope2VocabularyRegistry 词汇注册表:
>>> getVocabularyRegistry() <....Zope2VocabularyRegistry object at ...>
要加载额外的 ZCML,可以使用 configurationContext 资源:
>>> zope.STARTUP['configurationContext'] <zope.configuration.config.ConfigurationMachine object ...>
有关如何使用 zope.configuration 的详细信息,请参阅 zca.rst。
STARTUP 层不执行任何特定的测试设置或清理。这留给 INTEGRATION_TESTING 和 FUNCTIONAL_TESTING 层,或其他使用它们的层类(IntegrationTesting 和 FunctionalTesting)。
>>> zope.STARTUP.testSetUp() >>> zope.STARTUP.testTearDown()
层清理将重置环境。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zope.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds. >>> import Zope2 >>> Zope2._began_startup 0 >>> Zope2.DB is None True >>> Zope2.bobo_application is None True >>> list(getSiteManager().registeredAdapters()) [] >>> getVocabularyRegistry() <zope.schema.vocabulary.VocabularyRegistry object at ...>
集成测试
INTEGRATION_TESTING 用于简单的 Zope WSGI 集成测试。它扩展 STARTUP,以确保在每个测试之前开始一个事务,并在每个测试之后回滚。测试期间还提供了两个资源:app 和 request。它不管理任何层状态 - 仅实现测试生命周期方法。
注意: 通常 不会 将 INTEGRATION_TESTING 作为基础层使用。相反,您会使用 IntegrationTesting 类来创建自己的层,该层具有 INTEGRATION_TESTING 的测试生命周期语义。请参阅 plone.testing 的 README 文件以获取示例。
app 是应用程序根。在测试中,您应该使用这个资源而不是 zopeApp 上下文管理器(它仍然是设置持久固定值的最佳选择),因为 app 资源是层管理的交易的一部分。
request 是测试请求。它与 app.REQUEST 相同。
>>> "%s.%s" % (zope.INTEGRATION_TESTING.__module__, zope.INTEGRATION_TESTING.__name__,) 'plone.testing.zope.IntegrationTesting' >>> zope.INTEGRATION_TESTING.__bases__ (<Layer 'plone.testing.zope.Startup'>,) >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zope.INTEGRATION_TESTING, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zope.Startup in ... seconds. Set up plone.testing.zope.IntegrationTesting in ... seconds.
现在让我们模拟一个测试。在测试设置期间,将提供 app 资源。在测试中,您应该始终使用此资源来访问应用程序根。
>>> zope.STARTUP.testSetUp() >>> zope.INTEGRATION_TESTING.testSetUp()
测试现在可以检查和修改环境。
>>> app = zope.INTEGRATION_TESTING['app'] # would normally be self.layer['app'] >>> app.manage_addFolder('folder1') >>> 'acl_users' in app.objectIds() and 'folder1' in app.objectIds() True
请求也是可用的:
>>> zope.INTEGRATION_TESTING['request'] # would normally be self.layer['request'] <HTTPRequest, URL=http://nohost>
我们可以创建一个用户并使用 zope.login() 辅助函数模拟以该用户身份登录:
>>> app._addRole('role1') >>> ignore = app['acl_users'].userFolderAddUser('user1', 'secret', ['role1'], []) >>> zope.login(app['acl_users'], 'user1')
在zope.login()函数中,第一个参数是包含相关用户的用户文件夹。第二个参数是用户的名称。不需要提供密码。
>>> from AccessControl import getSecurityManager >>> getSecurityManager().getUser() <User 'user1'>
您可以使用zope.setRoles()辅助函数来更改用户的角色:
>>> sorted(getSecurityManager().getUser().getRolesInContext(app)) ['Authenticated', 'role1'] >>> zope.setRoles(app['acl_users'], 'user1', []) >>> getSecurityManager().getUser().getRolesInContext(app) ['Authenticated']
要再次成为匿名用户,请使用zope.logout():
>>> zope.logout() >>> getSecurityManager().getUser() <SpecialUser 'Anonymous User'>
在拆除时,事务将回滚:
>>> zope.INTEGRATION_TESTING.testTearDown() >>> zope.STARTUP.testTearDown() >>> 'app' in zope.INTEGRATION_TESTING False >>> 'request' in zope.INTEGRATION_TESTING False >>> with zope.zopeApp() as app: ... 'acl_users' in app.objectIds() and 'folder1' not in app.objectIds() True
让我们拆除层:
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zope.IntegrationTesting in ... seconds. Tear down plone.testing.zope.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds.
功能测试
FUNCTIONAL_TESTING层与INTEGRATION_TESTING层非常相似,并公开相同的固定和资源。然而,它具有不同的事务语义。INTEGRATION_TESTING创建单个数据库存储,并在每次测试后回滚事务。FUNCTIONAL_TESTING为每个测试创建全新的数据库存储(在基本固定之上堆叠)。这允许测试执行显式提交的代码,这通常对于端到端测试是必需的。缺点是每个测试的设置和拆除需要更长的时间。
注意:再次强调,您通常不会将FUNCTIONAL_TESTING用作基本层。相反,您会使用FunctionalTesting类来创建具有FUNCTIONAL_TESTING测试生命周期语义的自己的层。请参阅plone.testing的README文件以获取示例。
与INTEGRATION_TESTING一样,FUNCTIONAL_TESTING基于STARTUP。
>>> "%s.%s" % (zope.FUNCTIONAL_TESTING.__module__, zope.FUNCTIONAL_TESTING.__name__,) 'plone.testing.zope.FunctionalTesting' >>> zope.FUNCTIONAL_TESTING.__bases__ (<Layer 'plone.testing.zope.Startup'>,) >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zope.FUNCTIONAL_TESTING, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zope.Startup in ... seconds. Set up plone.testing.zope.FunctionalTesting in ... seconds.
现在让我们模拟一个测试。在测试设置时,app资源被提供。在测试中,您应始终使用它来访问应用程序根。可以使用request资源来访问测试请求。
>>> zope.STARTUP.testSetUp() >>> zope.FUNCTIONAL_TESTING.testSetUp()
测试现在可以检查和修改环境。它还可以提交事务。
>>> app = zope.FUNCTIONAL_TESTING['app'] # would normally be self.layer['app'] >>> app.manage_addFolder('folder1') >>> 'acl_users' in app.objectIds() and 'folder1' in app.objectIds() True >>> import transaction >>> transaction.commit()
在拆除时,数据库将被拆除。
>>> zope.FUNCTIONAL_TESTING.testTearDown() >>> zope.STARTUP.testTearDown() >>> 'app' in zope.FUNCTIONAL_TESTING False >>> 'request' in zope.FUNCTIONAL_TESTING False >>> with zope.zopeApp() as app: ... 'acl_users' in app.objectIds() and 'folder1' not in app.objectIds() True
让我们拆除层:
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zope.FunctionalTesting in ... seconds. Tear down plone.testing.zope.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds.
测试浏览器
FUNCTIONAL_TESTING层和FunctionalTesting层类是使用zope.testbrowser进行功能测试的基础。这模拟了一个网络浏览器,允许通过其用户界面对应用程序进行“端到端”测试。
要使用测试浏览器与FunctionalTesting层(例如默认的FUNCTIONAL_TESTING层实例),我们需要使用自定义浏览器客户端,这确保测试浏览器使用正确的ZODB,并且与测试代码适当隔离。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zope.FUNCTIONAL_TESTING, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zope.Startup in ... seconds. Set up plone.testing.zope.FunctionalTesting in ... seconds.
让我们模拟一个测试:
>>> zope.STARTUP.testSetUp() >>> zope.FUNCTIONAL_TESTING.testSetUp()
在测试中,我们可以创建一个测试浏览器客户端,如下所示:
>>> app = zope.FUNCTIONAL_TESTING['app'] # would normally be self.layer['app'] >>> browser = zope.Browser(app)
通常最好让Zope错误显示完整的跟踪信息:
>>> browser.handleErrors = False
我们可以在测试固定中添加。然而,为了使这些更改对测试浏览器可见,我们需要提交事务。
>>> _ = app.manage_addDTMLDocument('dtml-doc-1') >>> import transaction; transaction.commit()
现在我们可以通过测试浏览器查看它:
>>> browser.open(app.absolute_url() + '/dtml-doc-1') >>> 'This is the dtml-doc-1 Document.' in browser.contents True
测试浏览器集成将URL转换为请求,并将控制权传递给Zope的发布者。让我们检查查询字符串是否可用于输入处理:
>>> from urllib.parse import urlencode >>> _ = app.manage_addDTMLDocument('dtml-doc-2', file='<dtml-var foo>') >>> import transaction; transaction.commit() >>> qs = urlencode({'foo': 'boo, bar & baz'}) # sic: the ampersand. >>> browser.open(app.absolute_url() + '/dtml-doc-2?' + qs) >>> browser.contents 'boo, bar & baz'
测试浏览器还与迭代器一起工作。让我们用一个简单的使用迭代器的文件实现来测试这一点。
>>> from plone.testing.tests import DummyFile >>> app._setObject('file1', DummyFile('file1')) 'file1' >>> import transaction; transaction.commit() >>> browser.open(app.absolute_url() + '/file1') >>> 'The test browser also works with iterators' in browser.contents True
请参阅zope.testbrowser文档,了解有关如何使用浏览器客户端的更多信息。
在拆除时,数据库将被拆除。
>>> zope.FUNCTIONAL_TESTING.testTearDown() >>> zope.STARTUP.testTearDown() >>> 'app' in zope.FUNCTIONAL_TESTING False >>> 'request' in zope.FUNCTIONAL_TESTING False >>> with zope.zopeApp() as app: ... 'acl_users' in app.objectIds()\ ... and 'folder1' not in app.objectIds()\ ... and 'file1' not in app.objectIds() True
让我们拆除层:
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zope.FunctionalTesting in ... seconds. Tear down plone.testing.zope.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds.
HTTP 服务器
WSGI_SERVER_FIXTURE层扩展STARTUP以在单独的线程中启动单个线程的Zope服务器。这使得可以使用网络浏览器或Selenium或Windmill等测试工具连接到测试实例。
WSGI_SERVER层提供了一个基于WSGI_SERVER_FIXTURE的FunctionalTesting层。
>>> "%s.%s" % (zope.WSGI_SERVER_FIXTURE.__module__, zope.WSGI_SERVER_FIXTURE.__name__,) 'plone.testing.zope.WSGIServer' >>> zope.WSGI_SERVER_FIXTURE.__bases__ (<Layer 'plone.testing.zope.Startup'>,) >>> "%s.%s" % (zope.WSGI_SERVER.__module__, zope.WSGI_SERVER.__name__,) 'plone.testing.zope.WSGIServer:Functional' >>> zope.WSGI_SERVER.__bases__ (<Layer 'plone.testing.zope.WSGIServer'>,) >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zope.WSGI_SERVER, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zope.Startup in ... seconds. Set up plone.testing.zope.WSGIServer in ... seconds. Set up plone.testing.zope.WSGIServer:Functional in ... seconds.
在层设置之后,资源主机和端口可用,并指示Zope的运行位置。
>>> host = zope.WSGI_SERVER['host'] >>> host 'localhost' >>> port = zope.WSGI_SERVER['port']
现在我们来模拟一个测试。测试设置不超越基础层所做的内容。
>>> zope.STARTUP.testSetUp() >>> zope.FUNCTIONAL_TESTING.testSetUp() >>> zope.WSGI_SERVER.testSetUp()
在测试中,通常使用Python API来更改服务器的状态(例如创建一些内容或更改设置),然后使用HTTP协议查看结果。请注意,服务器正在单独的线程中运行,具有单独的安全管理器,因此对zope.login()和zope.logout()之类的调用不会影响服务器线程。
>>> app = zope.WSGI_SERVER['app'] # would normally be self.layer['app'] >>> _ = app.manage_addDTMLDocument('dtml-doc-3')
注意,在它出现在其他线程之前,我们需要提交事务。
>>> import transaction; transaction.commit()
现在我们可以通过服务器查找这个新对象。
>>> app_url = app.absolute_url() >>> app_url.split(':')[:-1] ['http', '//127.0.0.1'] >>> from urllib.request import urlopen >>> conn = urlopen(app_url + '/dtml-doc-3', timeout=5) >>> b'This is the dtml-doc-3 Document.' in conn.read() True >>> conn.close()
测试拆卸不超越基础层所做的内容。
>>> zope.WSGI_SERVER.testTearDown() >>> zope.FUNCTIONAL_TESTING.testTearDown() >>> zope.STARTUP.testTearDown() >>> 'app' in zope.WSGI_SERVER False >>> 'request' in zope.WSGI_SERVER False >>> with zope.zopeApp() as app: ... 'acl_users' in app.objectIds() and 'folder1' not in app.objectIds() True
当服务器拆卸时,WSGIServer线程停止。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zope.WSGIServer:Functional in ... seconds. Tear down plone.testing.zope.WSGIServer in ... seconds. Tear down plone.testing.zope.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds.
我们预计会看到以下异常之一:- URLError:<urlopen错误[Errno …]连接被拒绝> - 错误:[Errno 104]连接由对等方重置
>>> try: ... conn = urlopen(app_url + '/folder1', timeout=5) ... except Exception as exc: ... if 'Connection refused' not in str(exc) and 'Connection reset' not in str(exc): ... raise exc ... else: ... print('urlopen should have raised exception')
Zope 2 层
Zope 2层位于模块plone.testing.zserver中:
>>> from plone.testing import zserver
对于测试,我们需要一个测试运行器:
>>> from zope.testrunner import runner
启动
STARTUP是所有Zope 2测试的基础层。它设置了一个适合测试的Zope 2沙盒环境。它扩展了zca.LAYER_CLEANUP层,以最大化拥有和离开一个纯净环境的机会。
注意:您可能至少应该使用INTEGRATION_TESTING来进行任何实际测试,尽管如果您正在设置自己的固定件,STARTUP是一个有用的基础层。请参阅下面关于INTEGRATION_TESTING的描述。
>>> "%s.%s" % (zserver.STARTUP.__module__, zserver.STARTUP.__name__,) 'plone.testing.zserver.Startup' >>> zserver.STARTUP.__bases__ (<Layer 'plone.testing.zca.LayerCleanup'>,)
层设置时,Zope以轻量级的方式初始化。这包括对Zope管理的全局模块的某些修补,以减少设置时间,一个基于DemoStorage的数据库,以及Zope 2必须安装的一组最小产品。加载了一组最小的ZCML,但Products命名空间中的包不会自动配置。
让我们在测试之前验证我们有一个空的组件注册表:
>>> from zope.component import getSiteManager >>> list(getSiteManager().registeredAdapters()) []
Five在层设置时设置了一个特殊的词汇注册表,但在设置之前有一个默认的设置:
>>> from zope.schema.vocabulary import getVocabularyRegistry >>> getVocabularyRegistry() <zope.schema.vocabulary.VocabularyRegistry object ...> >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zserver.STARTUP, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zserver.Startup in ... seconds.
层设置后,zodbDB资源可用,指向默认的ZODB。
>>> zserver.STARTUP['zodbDB'] <ZODB.DB.DB object at ...> >>> zserver.STARTUP['zodbDB'].storage Startup
此外,资源host和port被设置为用于从Zope生成URL的默认主机名和端口号。这些是硬编码的,但被提供实际运行Zope实例的层所阴影。
>>> zserver.STARTUP['host'] 'nohost' >>> zserver.STARTUP['port'] 80
在此阶段,还可能获取一个Zope应用程序根。如果您正在设置层固定件,可以使用正确的数据库并通过使用zopeApp()上下文管理器来获取一个正确关闭的应用程序根。
>>> with zserver.zopeApp() as app: ... 'acl_users' in app.objectIds() True
如果您想使用特定的数据库,可以将它传递给 zopeApp() 函数作为 db 参数。将打开并关闭一个新连接。
>>> with zserver.zopeApp(db=zserver.STARTUP['zodbDB']) as app: ... 'acl_users' in app.objectIds() True
如果您想重用现有连接,可以将它传递给 zopeApp() 函数作为 connection 参数。在这种情况下,您需要自己关闭连接。
>>> conn = zserver.STARTUP['zodbDB'].open() >>> with zserver.zopeApp(connection=conn) as app: ... 'acl_users' in app.objectIds() True >>> conn.opened is not None True >>> conn.close()
如果在 with 块中抛出异常,事务将被中止,但连接仍然会被关闭(如果它是由上下文管理器打开的):
>>> with zserver.zopeApp() as app: ... raise Exception("Test error") Traceback (most recent call last): ... Exception: Test error
通常将 zopeApp() 上下文管理器与堆叠的 DemoStorage 结合使用,以设置特定层的固定值。如下所示:
from plone.testing import Layer, zserver, zodb class MyLayer(Layer): defaultBases = (zserver.STARTUP,) def setUp(self): self['zodbDB'] = zodb.stackDemoStorage(self.get('zodbDB'), name='MyLayer') with zserver.zopeApp() as app: # Set up a fixture, e.g.: app.manage_addFolder('folder1') folder = app['folder1'] folder._addRole('role1') folder.manage_addUserFolder() userFolder = folder['acl_users'] ignore = userFolder.userFolderAddUser('user1', 'secret', ['role1'], []) folder.manage_role('role1', ('Access contents information',)) def tearDown(self): self['zodbDB'].close() del self['zodbDB']
请注意,你通常在测试中不会使用zserver.zopeApp()或testSetUp()或testTearDown()方法。集成测试和功能测试层类为你管理应用程序对象,将它们作为资源app公开(见下文)。
层设置后,全局组件注册表中包含 Zope 需要的许多组件。
>>> len(list(getSiteManager().registeredAdapters())) > 1 # in fact, > a lot True
并且 Five 设置了一个 Zope2VocabularyRegistry 词汇注册表:
>>> getVocabularyRegistry() <....Zope2VocabularyRegistry object at ...>
要加载额外的 ZCML,可以使用 configurationContext 资源:
>>> zserver.STARTUP['configurationContext'] <zope.configuration.config.ConfigurationMachine object ...>
有关如何使用 zope.configuration 的详细信息,请参阅 zca.rst。
STARTUP 层不执行任何特定的测试设置或清理。这留给 INTEGRATION_TESTING 和 FUNCTIONAL_TESTING 层,或其他使用它们的层类(IntegrationTesting 和 FunctionalTesting)。
>>> zserver.STARTUP.testSetUp() >>> zserver.STARTUP.testTearDown()
层清理将重置环境。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zserver.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds. >>> import ZServer.Zope2 >>> ZServer.Zope2._began_startup 0 >>> import Zope2 >>> Zope2.DB is None True >>> Zope2.bobo_application is None True >>> list(getSiteManager().registeredAdapters()) [] >>> getVocabularyRegistry() <zope.schema.vocabulary.VocabularyRegistry object at ...>
集成测试
INTEGRATION_TESTING旨在进行简单的Zope 2集成测试。它扩展STARTUP以确保在每个测试之前开始事务并在测试之后回滚。在测试期间,还可以使用两个资源,即app和request。它不管理任何层状态 - 它仅实现测试生命周期方法。
注意: 通常 不会 将 INTEGRATION_TESTING 作为基础层使用。相反,您会使用 IntegrationTesting 类来创建自己的层,该层具有 INTEGRATION_TESTING 的测试生命周期语义。请参阅 plone.testing 的 README 文件以获取示例。
app 是应用程序根。在测试中,您应该使用这个资源而不是 zopeApp 上下文管理器(它仍然是设置持久固定值的最佳选择),因为 app 资源是层管理的交易的一部分。
request 是测试请求。它与 app.REQUEST 相同。
>>> "%s.%s" % (zserver.INTEGRATION_TESTING.__module__, zserver.INTEGRATION_TESTING.__name__,) 'plone.testing.zserver.IntegrationTesting' >>> zserver.INTEGRATION_TESTING.__bases__ (<Layer 'plone.testing.zserver.Startup'>,) >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zserver.INTEGRATION_TESTING, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zserver.Startup in ... seconds. Set up plone.testing.zserver.IntegrationTesting in ... seconds.
现在让我们模拟一个测试。在测试设置期间,将提供 app 资源。在测试中,您应该始终使用此资源来访问应用程序根。
>>> zserver.STARTUP.testSetUp() >>> zserver.INTEGRATION_TESTING.testSetUp()
测试现在可以检查和修改环境。
>>> app = zserver.INTEGRATION_TESTING['app'] # would normally be self.layer['app'] >>> app.manage_addFolder('folder1') >>> 'acl_users' in app.objectIds() and 'folder1' in app.objectIds() True
请求也是可用的:
>>> zserver.INTEGRATION_TESTING['request'] # would normally be self.layer['request'] <HTTPRequest, URL=http://nohost>
我们可以创建一个用户并使用zserver.login()辅助程序模拟作为该用户登录:
>>> app._addRole('role1') >>> ignore = app['acl_users'].userFolderAddUser('user1', 'secret', ['role1'], []) >>> zserver.login(app['acl_users'], 'user1')
zserver.login()的第一个参数是包含相关用户的用户文件夹。第二个参数是用户的名称。不需要提供密码。
>>> from AccessControl import getSecurityManager >>> getSecurityManager().getUser() <User 'user1'>
您可以使用zserver.setRoles()辅助程序更改用户的角色:
>>> sorted(getSecurityManager().getUser().getRolesInContext(app)) ['Authenticated', 'role1'] >>> zserver.setRoles(app['acl_users'], 'user1', []) >>> getSecurityManager().getUser().getRolesInContext(app) ['Authenticated']
要再次成为匿名用户,请使用zserver.logout():
>>> zserver.logout() >>> getSecurityManager().getUser() <SpecialUser 'Anonymous User'>
在拆除时,事务将回滚:
>>> zserver.INTEGRATION_TESTING.testTearDown() >>> zserver.STARTUP.testTearDown() >>> 'app' in zserver.INTEGRATION_TESTING False >>> 'request' in zserver.INTEGRATION_TESTING False >>> with zserver.zopeApp() as app: ... 'acl_users' in app.objectIds() and 'folder1' not in app.objectIds() True
让我们拆除层:
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zserver.IntegrationTesting in ... seconds. Tear down plone.testing.zserver.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds.
功能测试
FUNCTIONAL_TESTING层与INTEGRATION_TESTING层非常相似,并公开相同的固定和资源。然而,它具有不同的事务语义。INTEGRATION_TESTING创建单个数据库存储,并在每次测试后回滚事务。FUNCTIONAL_TESTING为每个测试创建全新的数据库存储(在基本固定之上堆叠)。这允许测试执行显式提交的代码,这通常对于端到端测试是必需的。缺点是每个测试的设置和拆除需要更长的时间。
注意:再次强调,您通常不会将FUNCTIONAL_TESTING用作基本层。相反,您会使用FunctionalTesting类来创建具有FUNCTIONAL_TESTING测试生命周期语义的自己的层。请参阅plone.testing的README文件以获取示例。
与INTEGRATION_TESTING一样,FUNCTIONAL_TESTING基于STARTUP。
>>> "%s.%s" % (zserver.FUNCTIONAL_TESTING.__module__, zserver.FUNCTIONAL_TESTING.__name__,) 'plone.testing.zserver.FunctionalTesting' >>> zserver.FUNCTIONAL_TESTING.__bases__ (<Layer 'plone.testing.zserver.Startup'>,) >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zserver.FUNCTIONAL_TESTING, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zserver.Startup in ... seconds. Set up plone.testing.zserver.FunctionalTesting in ... seconds.
现在让我们模拟一个测试。在测试设置时,app资源被提供。在测试中,您应始终使用它来访问应用程序根。可以使用request资源来访问测试请求。
>>> zserver.STARTUP.testSetUp() >>> zserver.FUNCTIONAL_TESTING.testSetUp()
测试现在可以检查和修改环境。它还可以提交事务。
>>> app = zserver.FUNCTIONAL_TESTING['app'] # would normally be self.layer['app'] >>> app.manage_addFolder('folder1') >>> 'acl_users' in app.objectIds() and 'folder1' in app.objectIds() True >>> import transaction >>> transaction.commit()
在拆除时,数据库将被拆除。
>>> zserver.FUNCTIONAL_TESTING.testTearDown() >>> zserver.STARTUP.testTearDown() >>> 'app' in zserver.FUNCTIONAL_TESTING False >>> 'request' in zserver.FUNCTIONAL_TESTING False >>> with zserver.zopeApp() as app: ... 'acl_users' in app.objectIds() and 'folder1' not in app.objectIds() True
让我们拆除层:
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zserver.FunctionalTesting in ... seconds. Tear down plone.testing.zserver.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds.
测试浏览器
FUNCTIONAL_TESTING层和FunctionalTesting层类是使用zope.testbrowser进行功能测试的基础。这模拟了一个网络浏览器,允许通过其用户界面对应用程序进行“端到端”测试。
要使用测试浏览器与FunctionalTesting层(例如默认的FUNCTIONAL_TESTING层实例),我们需要使用自定义浏览器客户端,这确保测试浏览器使用正确的ZODB,并且与测试代码适当隔离。
>>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zserver.FUNCTIONAL_TESTING, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zserver.Startup in ... seconds. Set up plone.testing.zserver.FunctionalTesting in ... seconds.
让我们模拟一个测试:
>>> zserver.STARTUP.testSetUp() >>> zserver.FUNCTIONAL_TESTING.testSetUp()
在测试中,我们可以创建一个测试浏览器客户端,如下所示:
>>> app = zserver.FUNCTIONAL_TESTING['app'] # would normally be self.layer['app'] >>> browser = zserver.Browser(app)
通常最好让Zope错误显示完整的跟踪信息:
>>> browser.handleErrors = False
我们可以在测试固定中添加。然而,为了使这些更改对测试浏览器可见,我们需要提交事务。
>>> app.manage_addFolder('folder1') >>> import transaction; transaction.commit()
现在我们可以通过测试浏览器查看它:
>>> browser.open(app.absolute_url() + '/folder1') >>> browser.contents.replace('"', '').replace("'", "") '<Folder ...'
Zope对象的__repr__不再稳定。
测试浏览器集成将URL转换为请求,并将控制权传递给Zope的发布者。让我们检查查询字符串是否可用于输入处理:
>>> import urllib >>> qs = urllib.urlencode({'foo': 'boo, bar & baz'}) # sic: the ampersand. >>> _ = app['folder1'].addDTMLMethod('index_html', file='<dtml-var foo>') >>> import transaction; transaction.commit() >>> browser.open(app.absolute_url() + '/folder1?' + qs) >>> browser.contents 'boo, bar & baz'
测试浏览器还与迭代器一起工作。让我们用一个简单的使用迭代器的文件实现来测试这一点。
>>> from plone.testing.tests import DummyFile >>> app._setObject('file1', DummyFile('file1')) 'file1' >>> import transaction; transaction.commit() >>> browser.open(app.absolute_url() + '/file1') >>> 'The test browser also works with iterators' in browser.contents True
请参阅zope.testbrowser文档,了解有关如何使用浏览器客户端的更多信息。
在拆除时,数据库将被拆除。
>>> zserver.FUNCTIONAL_TESTING.testTearDown() >>> zserver.STARTUP.testTearDown() >>> 'app' in zserver.FUNCTIONAL_TESTING False >>> 'request' in zserver.FUNCTIONAL_TESTING False >>> with zserver.zopeApp() as app: ... 'acl_users' in app.objectIds()\ ... and 'folder1' not in app.objectIds()\ ... and 'file1' not in app.objectIds() True
让我们拆除层:
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zserver.FunctionalTesting in ... seconds. Tear down plone.testing.zserver.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds.
HTTP 服务器
ZSERVER_FIXTURE层扩展STARTUP以在单独的线程中启动单个线程的Zope服务器。这使得可以使用Web浏览器或Selenium或Windmill之类的测试工具连接到测试实例。
ZSERVER层提供了一个具有ZSERVER_FIXTURE作为其基础的FunctionalTesting层。
>>> "%s.%s" % (zserver.ZSERVER_FIXTURE.__module__, zserver.ZSERVER_FIXTURE.__name__,) 'plone.testing.zserver.ZServer' >>> zserver.ZSERVER_FIXTURE.__bases__ (<Layer 'plone.testing.zserver.Startup'>,) >>> "%s.%s" % (zserver.ZSERVER.__module__, zserver.ZSERVER.__name__,) 'plone.testing.zserver.ZServer:Functional' >>> zserver.ZSERVER.__bases__ (<Layer 'plone.testing.zserver.ZServer'>,) >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zserver.ZSERVER, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zserver.Startup in ... seconds. Set up plone.testing.zserver.ZServer in ... seconds. Set up plone.testing.zserver.ZServer:Functional in ... seconds.
在层设置之后,资源主机和端口可用,并指示Zope的运行位置。
>>> host = zserver.ZSERVER['host'] >>> port = zserver.ZSERVER['port']
现在我们来模拟一个测试。测试设置不超越基础层所做的内容。
>>> zserver.STARTUP.testSetUp() >>> zserver.FUNCTIONAL_TESTING.testSetUp() >>> zserver.ZSERVER.testSetUp()
在测试中,通常使用Python API来更改服务器的状态(例如创建一些内容或更改设置),然后使用HTTP协议查看结果。请注意,服务器正在单独的线程中运行,具有单独的安全管理器,因此对zserver.login()和zserver.logout()之类的调用不会影响服务器线程。
>>> app = zserver.ZSERVER['app'] # would normally be self.layer['app'] >>> app.manage_addFolder('folder1')
注意,在它出现在其他线程之前,我们需要提交事务。
>>> import transaction; transaction.commit()
现在我们可以通过服务器查找这个新对象。
>>> app_url = app.absolute_url() >>> app_url.split(':')[:-1] ['http', '//127.0.0.1'] >>> import urllib2 >>> conn = urllib2.urlopen(app_url + '/folder1', timeout=5) >>> conn.read().replace('"', '').replace("'", "") '<Folder ...' >>> conn.close()
Zope对象的__repr__不再稳定。
测试拆卸不超越基础层所做的内容。
>>> zserver.ZSERVER.testTearDown() >>> zserver.FUNCTIONAL_TESTING.testTearDown() >>> zserver.STARTUP.testTearDown() >>> 'app' in zserver.ZSERVER False >>> 'request' in zserver.ZSERVER False >>> with zserver.zopeApp() as app: ... 'acl_users' in app.objectIds() and 'folder1' not in app.objectIds() True
当服务器拆卸时,ZServer线程停止。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zserver.ZServer:Functional in ... seconds. Tear down plone.testing.zserver.ZServer in ... seconds. Tear down plone.testing.zserver.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds.
我们预计会看到以下异常之一:- URLError:<urlopen错误[Errno …]连接被拒绝> - 错误:[Errno 104]连接由对等方重置
>>> try: ... conn = urllib2.urlopen(app_url + '/folder1', timeout=5) ... except Exception as exc: ... if 'Connection refused' not in str(exc) and 'Connection reset' not in str(exc): ... raise exc ... else: ... print('urllib2.urlopen should have raised exception')
FTP 服务器
FTP_SERVER层与ZSERVER相似,不同之处在于它启动FTP服务器而不是HTTP服务器。该设备包含在FTP_SERVER_FIXTURE层中。
警告:通常不建议同时运行 ZSERVER 和 FTP_SERVER 层,因为它们都会启动相同的 asyncore 循环。如果需要在测试中同时运行 HTTP 和 FTP 服务器,您可以创建自己的层,通过继承 ZServer 层类,并重写 setUpServer() 和 tearDownServer() 钩子来设置和关闭两个服务器。请参考代码示例。
FTP_SERVER_FIXTURE 层基于 STARTUP 层。
>>> "%s.%s" % (zserver.FTP_SERVER_FIXTURE.__module__, zserver.FTP_SERVER_FIXTURE.__name__,) 'plone.testing.zserver.FTPServer' >>> zserver.FTP_SERVER_FIXTURE.__bases__ (<Layer 'plone.testing.zserver.Startup'>,)
FTP_SERVER 层基于 FTP_SERVER_FIXTURE,使用 FunctionalTesting 层类。
>>> "%s.%s" % (zserver.FTP_SERVER.__module__, zserver.FTP_SERVER.__name__,) 'plone.testing.zserver.FTPServer:Functional' >>> zserver.FTP_SERVER.__bases__ (<Layer 'plone.testing.zserver.FTPServer'>,) >>> options = runner.get_options([], []) >>> setupLayers = {} >>> runner.setup_layer(options, zserver.FTP_SERVER, setupLayers) Set up plone.testing.zca.LayerCleanup in ... seconds. Set up plone.testing.zserver.Startup in ... seconds. Set up plone.testing.zserver.FTPServer in ... seconds. Set up plone.testing.zserver.FTPServer:Functional in ... seconds.
在层设置之后,资源主机和端口可用,并指示Zope的运行位置。
>>> host = zserver.FTP_SERVER['host'] >>> port = zserver.FTP_SERVER['port']
现在我们来模拟一个测试。测试设置不超越基础层所做的内容。
>>> zserver.STARTUP.testSetUp() >>> zserver.FUNCTIONAL_TESTING.testSetUp() >>> zserver.FTP_SERVER.testSetUp()
与 ZSERVER 一样,我们将为测试设置一些内容,然后通过 FTP 端口访问它。
>>> app = zserver.FTP_SERVER['app'] # would normally be self.layer['app'] >>> app.manage_addFolder('folder1')
我们还将创建根用户文件夹中的用户,以简化 FTP 访问。
>>> ignore = app['acl_users'].userFolderAddUser('admin', 'secret', ['Manager'], ())
注意,在它出现在其他线程之前,我们需要提交事务。
>>> import transaction; transaction.commit()
现在我们可以通过服务器查找这个新对象。
>>> app_path = app.absolute_url_path() >>> import ftplib >>> ftpClient = ftplib.FTP() >>> ftpClient.connect(host, port, timeout=5) '220 ... FTP server (...) ready.' >>> ftpClient.login('admin', 'secret') '230 Login successful.' >>> ftpClient.cwd(app_path) '250 CWD command successful.' >>> ftpClient.retrlines('LIST') drwxrwx--- 1 Zope Zope 0 ... . ...--w--w---- 1 Zope Zope 0 ... acl_users drwxrwx--- 1 Zope Zope 0 ... folder1 '226 Transfer complete' >>> ftpClient.quit() '221 Goodbye.'
测试拆卸不超越基础层所做的内容。
>>> zserver.FTP_SERVER.testTearDown() >>> zserver.FUNCTIONAL_TESTING.testTearDown() >>> zserver.STARTUP.testTearDown() >>> 'app' in zserver.ZSERVER False >>> 'request' in zserver.ZSERVER False >>> with zserver.zopeApp() as app: ... 'acl_users' in app.objectIds() and 'folder1' not in app.objectIds() True
当服务器被拆除时,FTP 线程会停止。
>>> runner.tear_down_unneeded(options, [], setupLayers, []) Tear down plone.testing.zserver.FTPServer:Functional in ... seconds. Tear down plone.testing.zserver.FTPServer in ... seconds. Tear down plone.testing.zserver.Startup in ... seconds. Tear down plone.testing.zca.LayerCleanup in ... seconds. >>> ftpClient.connect(host, port, timeout=5) Traceback (most recent call last): ... error: [Errno ...] Connection refused
项目详情
下载文件
下载适合您平台的文件。如果您不确定选择哪一个,请了解有关 安装包 的更多信息。