第 21 章 简单的社会化功能,页面模式,以及练习

我们就让用户能和其他人协作完成他们的列表。

在实现这个功能的过程中,先使用 Selenium 交互等待模式改进功能测试,然后试用页面对象模式(Page Object pattern)。

21.1 有多个用户以及使用 addCleanup 的功能测试

这个功能测试需要两个用户:

# functional_tests/test_sharing.py
from selenium import webdriver
from .base import FunctionalTest

def quit_if_possible(browser):
    try:
        browser.quit()
    except: pass

class SharingTest(FunctionalTest):
    def test_logged_in_users_lists_are_saved_as_my_lists(self):
        # Y 是已登录用户
        self.create_pre_authenticated_session("edith@example.com")
        edith_browser = self.browser
        self.addCleanup(lambda: quit_if_possible(edith_browser))

        # 她的朋友 Oniciferous 也在使用这个清单网站
        oni_browser = webdriver.Firefox()
        self.addCleanup(lambda: quit_if_possible(oni_browser))
        self.browser = oni_browser
        self.create_pre_authenticated_session("oniciferous@example.com")

        # Y 访问首页,新建一个清单
        self.browser = edith_browser
        self.browser.get(self.server_url)
        self.get_item_input_box().send_keys("Get help\n")

        # 她看到“分享这个清单”选项
        share_box = self.browser.find_element_by_css_selector("input[name=email]")
        self.assertEqual(
            share_box.get_attribute("placeholder"),
            "your-friend@example.com"
        )

有一个功能值得注意:addCleanup 函数,它的文档可以在这里arrow-up-right查看。这个函数可以代替 tearDown 函数,清理测试中使用的资源。如果资源在测试运行的过程中才用到,最好使用 addCleanup 函数,因为这样就不用在 tearDown 函数中花时间区分哪些资源需要清理,哪些不需要清理。

addCleanup 函数在 tearDown 函数之后运行,所以在 quit_if_possible 函数中才要使用 try/except 语句,因为不管 edith_browseroni_browser 中哪一个值是 self.browser,测试结束时 tearDown 函数都会关闭这个浏览器。

还要把测试方法 create_pre_authenticated_sessiontest_my_lists.py 中移到 base.py 中。

测试可以看到意料之中的失败,因为页面中没有填写邮件地址的输入框,无法分享给别人。

现在做一次提交,因为至少已经编写了一个占位功能测试,也移动了 create_pre_authenticated_session 函数,接下来要重构功能测试。

21.2 实现 Selenium 交互等待模式

先仔细看一下现在功能测试中与网站交互的代码:

与网站交互后,过多猜想浏览器的状态有风险。理论上,如果 find_element_by_css_selector 第一次没有找到 input[name=email],implicitly_wait 在后台会再试几次。但重试的过程中可能出错,假如前一个页面中也有属性为 name=email 的输入框,只是占位文本不同,测试会莫名其妙地失败,因为理论上,在新页面加载的同时,Selenium 也可以获取前一个页面中的元素,很可能会抛出 StaleElementException 异常。

如果 Selenium 意外抛出 StaleElementException 异常,通常是因为有某种条件竞争。或许应该使用显式等待模式。

因此,如果交互后想立即检查结果,一定要谨慎。可以沿用 wait_for 函数中使用的等待方式,改为:

21.3 页面模式

这里可以使用”三则重构“原则。这个测试以及很多测试,开头都是用户新建一个清单。定义一个辅助函数,命名为 start_new_list,让它调用 wait_for 以及输入清单中的待办事项。

分析功能测试的辅助代码有个公认可行的方式,叫做页面模式arrow-up-right。在页面模式中要定义多个对象,分别表示网站中不同的页面,而且只能在这些对象中存储于页面交互的方式。

首页的页面对象如下:

ListPage 类的定义如下:

一般来说,最好把页面对象放在各自的文件中。这里 HomePage 和 ListPage 联系比较紧密,所以可以放在同一个文件中。

下面看一下如何在测试中使用页面对象:

继续改写测试,只想访问列表页面中的元素,就使用页面对象:

我们要在 ListPage 类中添加以下三个方法:

页面模型背后的思想是,把网站中某个页面的所有信息都集中放在一个地方,如果以后想要修改这个页面,比如简单的调整 HTML 布局,功能测试只需改动一个地方。

接下来要继续重构其他功能测试。

21.4 扩展功能测试测试第二个用户和 ”My Lists“ 页面

把分享功能的用户故事写得更加详细一点。Y 在她的清单页面看到这个清单已经分享给 Oniciferous,然后 Oniciferous 登录,看到这个清单出现在 ”My Lists“ 页面中,或许显示在 ”分享给我的清单“ 中:

为此,要在 HomePage 类中再定义一个方法:

这个方法最好放在 test_my_lists.py 中,或许还可以再定义一个 MyListsPage 类。

现在,Oniciferous 也可以在这个清单中添加待办事项:

为此,要在页面对象中再定义几个方法:

接下来运行功能测试,看看这些测试能否通过。

得到预料之中的失败,因为还没在页面中添加输入框,填写电子邮件地址,分享给别人,做次提交:

21.5 留给读者的练习

实现这个新功能所需的步骤大致如下:

  1. 在 list.html 添加一个新区域,先写一个表单,表单中包含一个输入框,用来输入电子邮件地址。功能测试应该会前进一步

  1. 需要一个视图,处理表单。先在模板中定义 URL,例如 lists//share

  2. 然后,编写第一个单元测试,驱动我们定义占位视图。我们希望这个视图处理 POST 请求,响应是重定向,指向清单页面,所以这个测试可以命名为 ShareListTest.test_post_redirects_to_lists_page

  1. 编写占位视图,只需两行代码,一行用于查找清单,一行用于重定向

  2. 可以再编写一个单元测试,在测试中创建一个用户和一个清单,在 POST 请求中发送电子邮件地址,然后检查 list_.shared_with.all() (类似于 "My Lists" 页面使用的那个 ORM 用法)中是否包含这个用户。shared_with 属性还不存在,我们使用的是由外而内的方式

  3. 所以在这个测试通过之前,要下移到模型层。下一个测试要写入 test_models.py 中。在这个测试中,可以检查清单能否响应 shared_with.add 方法。这个方法的参数是用户的电子邮件地址。然后检查清单的 shared_with.all() 查询集合中是否包含这个用户。

  4. 然后需要用到 ManyToManyField。或许你会看到一个错误消息,提示 related_name 有冲突,查阅 Django 的文档之后你会找到解决办法。

  5. 需要执行一次数据库迁移

  6. 然后,模型测试应该可以通过。回过头来修正视图测试

  7. 可能会发现重定向视图的测试失败,因为视图发送的 POST 请求无效。可以选择忽略无效的输入,也可以调整测试,发送有效的 POST 请求。

  1. 然后回到模板层。"My Lists" 页面需要一个 <ul> 元素,使用 for 循环列出分享给这个用户的清单。还想在清单页面显示这个清单分享给谁了,并注明这个清单的属主是谁。各元素的类和 ID 参加功能测试。如果需要,还可以为这几个需求编写简单的单元测试。

  1. 执行 runserver 命令让网站运行起来,或许能帮助你解决问题,以及调整布局和外观。如果使用隐私浏览器会话,可以同时登陆多个用户。

页面模式以及真正留给读者的练习

  • 在功能测试中运用 DRY 原则

    • 功能测试多起来后,就会发现不同的测试使用了 UI 的同一部分。尽量避免在多个功能测试中使用重复的常量,例如某个 UI 元素 HTML 代码中的 ID 和 类。

  • 页面模式

    • 把辅助方法移到 FunctionalTest 基类中会把这个类变得臃肿不抗。可以考虑把处理网站特定部分的全部逻辑保存到单独的页面对象中。

Last updated