7. 测验

7.1. 单元测试

这个 CubicWeb 框架提供 cubicweb.devtools.testlib.CubicWebTC 测试基类。

测试应放在mycube/test目录中。其他测试数据应进入mycube/测试/数据。

建议编写有关实体方法、操作、挂钩和操作、安全性的测试。这个 CubicWebTC 基类有一些方便的方法来帮助测试所有这些。

在视图领域,自动测试检查视图是否是有效的XHTML。参见 自动视图测试 有关详细信息。

大多数单元测试都需要一个实时数据库来进行工作。这是通过使用自动sqlite(与python捆绑在一起,请参见http://docs.python.org/library/sqlite3.html)作为后端实现的。

数据库存储在mycube/test/tmpdb、mycube/test/tmpdb模板文件中。如果它还不存在,它将在测试套件启动时自动构建。

警告

每当模式更改(新实体、属性、关系)时,必须删除这两个文件。只涉及实体或关系类型属性(约束、基数、权限)的更改,通常使用 sync_schema_props_perms() 迁移环境的功能不需要数据库重新生成步骤。

7.1.1. 单元测试示例

我们从从关键字cube提取的示例开始(可从http://www.cubicweb.org/project/cubicweb关键字获得)。

from cubicweb.devtools.testlib import CubicWebTC
from cubicweb import ValidationError

class ClassificationHooksTC(CubicWebTC):

    def setup_database(self):
        with self.admin_access.repo_cnx() as cnx:
            group_etype = cnx.find('CWEType', name='CWGroup').one()
            c1 = cnx.create_entity('Classification', name=u'classif1',
                                   classifies=group_etype)
            user_etype = cnx.find('CWEType', name='CWUser').one()
            c2 = cnx.create_entity('Classification', name=u'classif2',
                                   classifies=user_etype)
            self.kw1eid = cnx.create_entity('Keyword', name=u'kwgroup', included_in=c1).eid
            cnx.commit()

    def test_cannot_create_cycles(self):
        with self.admin_access.repo_cnx() as cnx:
            kw1 = cnx.entity_from_eid(self.kw1eid)
            # direct obvious cycle
            with self.assertRaises(ValidationError):
                kw1.cw_set(subkeyword_of=kw1)
            cnx.rollback()
            # testing indirect cycles
            kw3 = cnx.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
                              'SK subkeyword_of K WHERE C name "classif1", K eid %(k)s'
                              {'k': kw1}).get_entity(0,0)
            kw3.cw_set(reverse_subkeyword_of=kw1)
            self.assertRaises(ValidationError, cnx.commit)

测试类定义了 setup_database() 用初始数据填充数据库的方法。类的每个测试都使用这个预填充的数据库运行。

测试用例本身检查操作是否完成了防止关键字实体之间循环的工作。

这个 create_entity 连接(或请求)对象的方法允许创建实体。通过将此实体指定为参数、关系名称和要链接的实体指定为值,可以将此实体链接到其他实体。在上面的示例中, Classification 实体链接到 CWEtype 通过关系 classifies .相反,如果您正在创建 CWEtype 实体,可以将其链接到 Classification 实体,通过添加 reverse_classifies 作为争论。

注解

这个 commit() 方法不是自动调用的。如果需要,您必须显式地调用它(尤其是测试操作)。用 entity_from_eid() 在提交后避免请求缓存效果。

您可以在 步骤1:在架构中配置安全性 .

可以使用 apycot .

7.1.1.1. 管理连接或用户

由于单元测试是使用sqlite后端完成的,并且一次不支持多个连接,因此在模拟安全性和更改用户时必须小心。

默认情况下,使用具有管理权限的用户运行测试。使用这些凭据的连接可以通过 admin_access 测试类的对象。

这个 repo_cnx() 方法返回可用作上下文管理器的连接对象:

# admin_access is a pre-cooked session wrapping object
# it is built with:
# self.admin_access = self.new_access('admin')
with self.admin_access.repo_cnx() as cnx:
    cnx.execute(...)
    self.create_user(cnx, login='user1')
    cnx.commit()

user1access = self.new_access('user1')
with user1access.web_request() as req:
    req.execute(...)
    req.cnx.commit()

退出上下文管理器时,将发出回滚,从而释放连接。别忘了发布 cnx.commit() 电话!

警告

不要使用保存在与另一个实体建立连接的实体中的引用!

7.1.2. 电子邮件通知测试

运行测试时,可能生成的电子邮件不会真正发送,而是在列表中找到 MAILBOX 模块的 cubicweb.devtools.testlib .

您可以通过分析此列表的内容来测试通知,其中包含具有两个属性的对象:

  • recipients ,收件人列表

  • msg ,email.message对象

让我们来看一个简单的例子 blog 立方体。

from cubicweb.devtools.testlib import CubicWebTC, MAILBOX

class BlogTestsCubicWebTC(CubicWebTC):
    """test blog specific behaviours"""

    def test_notifications(self):
        with self.admin_access.web_request() as req:
            cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
                                description=u'cubicweb is beautiful')
            blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
                                             content=u'cubicweb hop')
            blog_entry_1.cw_set(entry_of=cubicweb_blog)
            blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
                                             content=u'cubicweb yes')
            blog_entry_2.cw_set(entry_of=cubicweb_blog)
            self.assertEqual(len(MAILBOX), 0)
            req.cnx.commit()
            self.assertEqual(len(MAILBOX), 2)
            mail = MAILBOX[0]
            self.assertEqual(mail.subject, '[data] hop')
            mail = MAILBOX[1]
            self.assertEqual(mail.subject, '[data] yes')

7.1.3. 可见动作测试

很容易编写单元测试来测试用户或用户类别可见的操作。让我们以 conference cube .

class ConferenceActionsTC(CubicWebTC):

    def setup_database(self):
        with self.admin_access.repo_cnx() as cnx:
            self.confeid = cnx.create_entity('Conference',
                                             title=u'my conf',
                                             url_id=u'conf',
                                             start_on=date(2010, 1, 27),
                                             end_on = date(2010, 1, 29),
                                             call_open=True,
                                             reverse_is_chair_at=chair,
                                             reverse_is_reviewer_at=reviewer).eid

    def test_admin(self):
        with self.admin_access.web_request() as req:
            rset = req.find('Conference').one()
            self.assertListEqual(self.pactions(req, rset),
                                  [('workflow', workflow.WorkflowActions),
                                   ('edit', confactions.ModifyAction),
                                   ('managepermission', actions.ManagePermissionsAction),
                                   ('addrelated', actions.AddRelatedActions),
                                   ('delete', actions.DeleteAction),
                                   ('generate_badge_action', badges.GenerateBadgeAction),
                                   ('addtalkinconf', confactions.AddTalkInConferenceAction)
                                   ])
            self.assertListEqual(self.action_submenu(req, rset, 'addrelated'),
                                  [(u'add Track in_conf Conference object',
                                    u'http://testing.fr/cubicweb/add/Track'
                                    u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'
                                    u'__redirectpath=conference%%2Fconf&'
                                    u'__redirectvid=' % {'conf': self.confeid}),
                                   ])

您只需执行与您要测试的视图对应的RQL查询,并比较 pactions() 具有必须在接口中可见的操作列表。这是元组列表。第一个元素是动作的 __regid__ 第二个是动作课。

要测试子菜单中的操作,只需测试 action_submenu() 方法。方法的最后一个参数是操作的类别。结果是一个元组列表。第一个元素是操作的标题,第二个元素是操作的URL。

7.2. 自动视图测试

这是自动完成的 cubicweb.devtools.testlib.AutomaticWebTest 类。在创建多维数据集时,mycube/test/test_mycube.py文件包含这样的测试。这里的代码必须取消注释才能使用,无需进一步修改。

这个 auto_populate 方法使用智能算法在数据库中创建伪随机数据,从而可以调用和测试视图。

根据模式、挂钩和操作约束,自动填充并不总是可以继续进行的。

当然,可以完全重新定义自动填充。一个更简单的解决方案是给出关于自动填充机制必须跳过哪些实体和关系的提示(填充一些类属性)。这些是:

  • no_auto_populate ,可以包含要跳过的实体类型列表

  • ignored_relations ,可以包含要跳过的关系类型列表

  • application_rql ,可能包含一个RQL表达式列表,自动填充不能自行猜测;这些表达式必须生成结果集,可以根据这些结果集选择视图。

警告

注意不要让进口的 AutomaticWebTest 在测试模块名称空间中,否则两个子类 and 将运行此父类。

7.3. 缓存重型数据库设置

一些测试套件需要对数据库进行复杂的设置,这需要几秒钟(甚至几分钟)才能完成。为每个单独的测试进行整个设置会使整个运行非常缓慢。这个 CubicWebTC 类提供了一种简单的方法,可以为多个测试一次准备特定的数据库。这个 test_db_id 你的类属性 CubicWebTC 子类必须设置为唯一标识符,并且 pre_setup_database() 类方法必须生成缓存的内容。作为 pre_setup_database() 方法不允许在每次运行测试方法时调用,您不能将任何类属性设置为在测试期间使用 那里 .每个数据库 test_db_id 如果不在缓存中,则自动创建。清除缓存取决于用户。缓存文件位于 data/database 测试目录的子目录。

警告

注意保持不变 pre_setup_database() 所有类的函数 test_db_id 否则,根据第一次遇到的结果,您的测试将有不可预测的结果。

7.4. 实际数据库测试

这个 CubicWebTC 类使用 cubicweb.devtools.ApptestConfiguration 配置类以设置其测试环境(数据库驱动程序、用户密码、应用程序主页等)。这个 cubicweb.devtools 模块还提供 RealDatabaseConfiguration 类,该类将读取常规CubicWeb源文件以获取所有这些信息,但也将阻止在测试之间初始化和重置数据库。

为了让测试类使用特定的配置,必须设置 _config 类的类属性,如:

from cubicweb.devtools import RealDatabaseConfiguration
from cubicweb.devtools.testlib import CubicWebTC

class BlogRealDatabaseTC(CubicWebTC):
    _config = RealDatabaseConfiguration('blog',
                                        sourcefile='/path/to/realdb_sources')

    def test_blog_rss(self):
        with self.admin_access.web_request() as req:
            rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
                               'B created_by U, U login "logilab", B creation_date D')
            self.view('rss', rset, req=req)

7.5. 用其他立方体测试

有时一个小组件不能全部单独测试,所以需要指定其他多维数据集作为单元测试套件的一部分。这是由 bootstrap_cubes 文件位于 mycube/test/data .一个例子来自 preview 立方体:

card, file, preview

格式为:

  • 可能有几条Empy线或几条以 # (注释行)

  • 一行,包含以逗号分隔的多维数据集名称列表。

也可以添加 schema.py 文件在 mycube/test/data 将由测试框架使用,因此使新的实体类型和关系可用于测试。

7.6. 识字编程

CubicWeb提供了一些识字的编程功能。这个 cubicweb-ctl 工具 shell 命令接受不同的文件格式。如果你的文件以 .txt.rst ,文件将由 doctest.testfile 带CubicWeb 迁移 API已在其中启用。

创建一个 scenario.txt file in the test/ directory and fill with some content. Refer to the doctest.testfile documentation .

然后,您可以通过以下方式直接运行它:

$ cubicweb-ctl shell <cube_instance> test/scenario.txt

当场景文件准备好后,将其放入新的测试用例中,以便能够自动运行它。

from os.path import dirname, join
from logilab.common.testlib import unittest_main
from cubicweb.devtools.testlib import CubicWebTC

class AcceptanceTC(CubicWebTC):

        def test_scenario(self):
                self.assertDocTestFile(join(dirname(__file__), 'scenario.txt'))

if __name__ == '__main__':
        unittest_main()

7.6.1. 跳过方案

如果要设置不能放入单元测试用例中的初始条件,则必须使用 KeyboardInterrupt 例外只是因为 doctest 模块将在内部捕获所有异常。

>>> if condition_not_met:
...     raise KeyboardInterrupt('please, check your fixture.')

7.6.2. 通过参数

使用额外的参数来参数化您的场景是可能的,方法是用双破折号将其前置。

请参阅 cubicweb-ctl shell --help 用法。

重要

您的方案文件必须是UTF-8编码的。

7.7. 测试API

7.7.1. 使用Pytest

这个 pytest utility (shipping with logilab-common 是CubicWeb的强制依赖项)扩展了python单元测试功能,是运行CubicWeb测试套件的首选方法。裸单元测试也以通常的方式工作。

要使用它,您可以:

  • 刚刚启动 pytest 在多维数据集中执行所有测试(它将自动发现这些测试)

  • 发射 pytest unittest_foo.py 执行一个测试文件

  • 发射 pytest unittest_foo.py bar 执行所有测试方法和名称中包含 bar

另外, -x 选项告诉pytest在第一个错误或失败时退出。这个 -i 选项告诉Pytest在测试中发生异常时将其放入PDB。

-x 选项已被使用,运行在测试中停止,修复测试后,可以使用 -R 告诉它从以前失败的地方重新开始测试的选项。

7.7.2. 使用 TestCase 基类

CubicWebTC的基类是logilab.common.testlib.testcase,它提供了许多方便的断言方法。

7.7.3. CubicWebTc应用程序接口

7.8. 关于请求和会话,您需要知道什么

../../_images/request_session.png

首先,请记住,有些代码运行在客户机端,另一些运行在存储库端。更准确地说:

  • 客户端:Web接口、原始repapi连接(例如cubicWeb ctl shell);

  • 存储库端:rql查询执行,可能触发钩子和操作。

客户机通过repapi连接与存储库交互。

注解

这些区别将在CubicWeb 3.21中消失(如果之前没有)。

repoapi连接绑定到存储库中的会话。连接和请求对象无法从存储库代码访问/会话对象无法从客户端代码访问(理论上至少如此)。

Web界面提供了一个请求类。那个 request 对象提供对所有CubicWeb资源的访问,例如:

  • 注册表(它本身提供对模式和配置的访问);

  • 基础的repoapi连接(当使用req.execute时,您实际上调用repoapi);

  • 其他特定资源取决于客户端类型(根据基本URL、表单参数等生成URL)。

A session 提供一个类似于RQL执行和访问全球资源(注册表和所有)请求的API,但还具有以下职责:

  • 处理事务数据,这些数据将在单个事务期间存在。这包括将用于执行RQL查询的数据库连接。

  • 处理可以跨不同(Web)请求使用的持久数据

  • 安全和挂钩控制(不能通过请求)

7.8.1. 这个 _cw 属性

这个 _cw 每个应用程序对象上的可用属性提供对所有CubicWeb资源的访问,即:

  • 对于在客户端运行的代码(例如Web界面视图), _cw 是请求实例。

  • 对于在存储库端运行的代码(钩子和操作), _cw 是连接或会话实例。

注意,某些视图可能是通过会话(例如通知)或请求调用的。

7.8.2. 请求、会话和事务

在Web界面中,HTTP请求由单个请求处理,一旦发送响应,该请求将被丢弃。

Web发布者处理事务:

  • 自动完成提交/回滚

  • 除非确实需要,否则不应显式提交/回滚

让我们详细说明这个过程:

  1. 传入的RQL查询来自客户端到Web堆栈

  2. Web堆栈打开与用户会话关联的请求的已验证数据库连接。

  3. 执行查询(通过存储库连接)

  4. 此查询可能触发挂钩。钩子和操作可以通过 cnx.execute .

  5. 存储库在1中获取查询结果。如果是RQL读取查询,则释放数据库连接。如果它是一个写查询,那么连接将绑定到会话,直到提交或回滚事务为止。

  6. 结果将被发送回客户端

这意味着以下几点:

  • 当使用一个请求或在钩子中执行的代码时,这个数据库连接处理是完全透明的。

  • 但是,编写测试时要小心:您通常是在伪造/测试服务器和客户端,因此您必须决定何时使用repoaccess.client_cnx或repoaccess.repo_cnx。问自己“我要测试的代码在哪里运行,客户端还是存储库端?”响应通常是:使用repo(因为“客户机连接”概念会在几个版本中消失)。