Skip to content

lluxury/python-tdd-book

Repository files navigation

还是挺厉害的,上一次做tdd做到158页了,但是没有印象,估计后期抄代码了
这次从头开始吧,补充进去, 建了一个skill ,用tdd的方式完成需求

plan模式
(没有TDD )
给出目标,数据存入数据库,分以下步骤:
在服务器中处理POST请求
1 新建测试,判断homepage是否能保存post请求,并运行测试
2 修改测试,判断渲染返回值(render_to_string),再断言
3 修改 assertTrue测试,尝试使用assertIn
4 新建一个测试,在新框输入 Use peacock ,看原有的测试是否都生效
5 重构功能测试,检查一行是否在list表中,刷新应该出现2行

Django ORM和第一个模型
1 需要使用Model了,先键一个测试,分别存入2个值(Item),检查是否有2个,并取出值判断是否正确
2 运行测试
3 第一个数据库迁移  makemigrations
4 测试lists
5 定义文本字段, 测试
6 把POST请求中的数据存入数据库 修改前面homepage save post的测试,改为从数据库中读取,并断言
7 新建一个测试,确保不要每次请求都保存空白项,并测试
8 新建测试,处理完POST请求后重定向到根目录,并测试
9 新建测试,在模板中断言2个渲染待办事项,(前面的测试生成了2个)
10 运行功能测试,如果失败运行 migrate迁移
11 解决待办事项前面没有序号的问题
12 解决测试遗留数据的问题 (--noinput)

1把functional_tests.py 移到functional_tests目录下, 以使用LiveServerTestCase,并让测试运行起来
2功能测试完不应该在页面遗留 代办事项
2.1 post后跳转,每个 清单唯一的URL和标识
2.2 新建 ListViewTest测试类,用Django测试客户端测试地址
2.3更新URL,视图,模版以通过测试
2.4 使用Django测试客户端重写之前post和重定向的测试,以测试新建清单
2.5 调整模型的单元测试,新建list对象,通过给.list赋值把2个代办事项归在名下
要检查清单是否正常保存,是否保存了代办事项与清单的关系
2.6为测试中的两个代办事项创建父清单
2.7 修改测试项,两个测试指向新URL,并只显示本清单包含的项目
2.8 新建2个客户端测试,把新代办事项加入现有清单
3修改之前测试,输放买羽毛后,跳转到新URL,检测URL是否符合期望值
4新用户访问,使用新的浏览器和session
5确认看不到前一个用户的内容
6新用户输入 代办事项
7新用户获得唯一 URL

  1. 新建一个测试,调用selenium 访问LiveServerUrl,窗口大小1024x768,需要输入框完美居中显示,并运行该测试,
  2. 继续上一个测试,输入清单testing,输入框仍然完美居中显示
  3. 给项目安装并配置Bootstrap
  4. 使用模板继承,建立base.html,再次运行测试
  5. 在base.html中加入Bootstrap,重写输入框,运行测试
  6. 定义 /static/ 前缀是静态资源,运行测试
  7. 换用StaticLiveServerTestCase 运行测试x
  8. Django 5.x移除了StaticLiveServerTestCase
  9. 使用Bootstrap中的超大文本块,大型输入框,样式化表格
  10. 任务8 使用Bootstrap的超大文本块(jumbotron没有使用)、大型输入框(input-lg同样)、样式化表格(table类) ,因为版本移除改变了写法
  11. 自己编写css,让输入表单离标题文字远一点,运行测试
  12. 使用collectstatic命令把静态文件移到项目之外,不进git仓库
  13. 输入完结果显示在下方应该也是居中的,并且前面有数字冒号空格,不知道是哪里遗漏了,帮忙补上测试再撰写代码

10
12. 新加一个测试,不能提交空待办事项,先不写内容,空函数
13. 测试失败,再用 skip标记跳过该测试
14. 把功能测试分拆到多个文件中,类NewVisitorTest,test_layout_and_styling,ItemValidationTest 等,原来的test.py 改为 base.py 只保留FunctionalTest 测试
15. 取消 skip标记,运行不能提交空待办事项的测试
16. 填充该功能测试,内容为访问页面,提交空事项并回车,显示错误.has-error,不能为空, 输入文字提交通过,再次提交空事项,再次提示错误,输入文字提交正常(没事不要搞太绕的场景cpu都冒烟了)
17. 重构单元测试,分拆成多个文件, 测试有单独的文件夹,视图测试,模型测试,运行测试
18. 模型测试添加单元测试,创建空的待办事项,使用self.assertRaises
19. 测试,迁移数据库
20. 在单元测试的视图测试里,加一个测试模块,检查是否有报错信息发向视图模板
21. 更新测试,确保无效的值不会存入数据库
22. 在渲染表单的视图中处理POST 请求,把new_item实现的功能移到view_list 中,在view_list 视图中执行模型验证
23. 重构:去除硬编码的URL,模板标签{% url %},重定向时使用get_absolute_url

11

  1. 使用单元测试探索表单API, ItemForm, form.as_p,并测试
  2. 写一个单元测试检查placeholder属性和css类
  3. 换用Django中的ModelForm类
  4. 测试ModelForm是否应用了模型中定义的验证规则(空提交,is_valid函数)
  5. 使用 EMPTY_LIST_ERROR
  6. 在处理GET请求的视图中使用这个表单, 修改视图的单元测试,使用测试客户端编写新测试,替代 home_page_returns_rorrect_html,和 test_root_url_resolves_to_home_page_view,旧测试先不删除
    1. 在第6项任务中,我发现 lists/templates/base.html 中没有使用input也没有换成 form.text, 是漏掉了,还是版本原因,这块是怎么实现的
  7. 测试通过后,替换旧测试的相关位置(例如 id_new_item)
  8. 在base.py 添加一个辅助方法,在需要替换的地方使用辅助方法
  9. 在处理POST请求的视图中使用这个表单,测试NewListTest,修改new_list 试图的单元测试(有验证错误,渲染首页,返回200,响应包含错误,表单对象传入模板,不用硬编码,用常量
  10. 在视图中使用这个表单,在模板中显示错误消息
  11. 写测试view_list 处理get表单请求,post请求,辅助方法 post_invalid_input,表单提交失败测试拆分,post,存入数据库,渲染,pass,显示的报错,并测试,
    1. 漏掉了辅助方法和数据库没修改的测试
  12. 测试保存handles 到 list,更新保存方法,并用instance指定
  13. 重构视图

12

  1. 建立针对重复事项的功能测试,可以输入“buy sth”,成功后再输入“buy sth”,的形式来测试 ,对这种情况需要弹出错误提示
  2. 在模型层建立同样情况测试,禁止重复 ,
  3. 同样位置再建立个测试,同样内容可以存入不同list中
  4. 使用Meta类在models模块实现单个list中事件唯一的约束
  5. 测试list中事项排序,可能查询集合要转成列表才能比较
  6. 重写旧模型测试,删除test_saving_and_retrieving_items,或类似测试,换成 test_default_text,test_item_is_related_to_list, 并改成针对Item和List的测试
  7. 测试保存时显示完整性错误, 在模块测试,检查重复事项的测试里,更新出保存事项,如果报错就改回去
  8. 在视图层检测待办事项重复验证,如果检测到,暂时skip跳过,不修复
  9. ExistingListItemForm测试,分别检查渲染输入项,验证空的项,重复字段的项
  10. 在清单视图中使用ExistingListItemForm, 去掉任务8加的skip标记,使用任务9建立的测试,修复view测试 test_display_item_form, test_for_invalid_input_passes_form_to_template
  11. 编写单元测试,test_form_save,
    models.py 有一些差异,但claude说,不是当前任务的需求造成的
    确实 test_duplicate_item_validation_errors_and_up_on_lists_pages claude说其他测试功能包含

13

  1. 新建功能测试,新建清单不当,出现验证错误,在输入框输入内容,错误消失
  2. 多次使用CSS查找错误消息元素,所以建了个辅助函数,并应用起来
  3. 在 lists/static/tests 下,布署Qunit,并生成一个tests.html,里面放一个冒烟测试
  4. 在tests.html中加入jQuery:错误可见,隐藏错误,错误不可见
  5. 使用 qunit-fixture 来帮助完成测试
  6. 代码命名为 list.js 放在单独的js中,测试
  7. 在所有页面引用脚本和jQuery
  8. 添加onload样板代码和命名空间 
    js的测试确实不太好做,或者说不熟悉

15
建一个测试分支

  1. 探究代码, 在lists/templates/base.html 中加入Supabase Auth代码,并把navigator.id 的 request方法绑到登录链接上
  2. 启动django的账户系统应用,处理 post请求 accounts/views.py 及认证代码,自定义用户模型,模型管理器,退出视图,URL 映射
  3. 新建功能测试,访问,登录,出现登录框,使用邮箱地址登录,mockmyid.com,点击后,窗口关闭,登录成功,用到2个辅助函数,循环切到新窗口,显式等待,并测试
  4. 测试成功后,回到主分支,删除探究代码 accounts,用测试驱动开发
  5. 在superlists/superlists 中建全站共用静态文件目录,调用现有单元测试,
  6. 建accounts应用,及js的test目录,除取客户端 JavaScript中的探究代码,换Supabase 代码,
  7. 使用模拟技术,给lists/templates/base.html 中把navigator传入初始化函数
  8. 在initialize函数的单元测试中使用mock,确保requestWasCalled 初始值是 false
  9. 使用sinon.js 创建mock,辅助函数看当前用户
  10. 建立测试,initialize calls navigator.id.watch 之前加入module函数
  11. 重写前面的2个测试,测试onlogin回掉函数
  12. 建立测试 onlogin post failure should do navigator.id.logout
    完成第12个任务后,如果 test functional_tests.test_login 失败,暂不处理
    Persona 服务已经下线了,但测试中没有测出来,需要关注

16

  1. 在Python代码中使用模拟技术,删除默认 accounts/tests/ 目录,新建一个,新建 LoginViewTest 测试,通过模拟authenticate函数测试视图
  2. 加一个测试,找到用户就返回ok
  3. 加测试,如果认证反回用户,获取登录session
  4. 加测试,如果认证返回None,客户端不在登录状态
  5. 模拟网络请求,测试带着域名发送assertion 到 mozilla,检查响应,提取邮件地址,如果通过不了,模拟一个数据,继续下一项测试
  6. 测试,上一个测试访问响应失败或有错误则返回None,用和上一个相同的请求
  7. 测试响应json中的状态是否为okay
  8. 检测是否能通过email寻找到现有用户,也许需要setUp辅助函数,在测试前保证有一个用户
  9. 测试传入数据persona确认有效(模拟),但数据库没有用户,创建新用户
  10. 测试通过email查找用户和找不到用户返回none
  11. 测试模型,不保存用户姓和名只需要邮件地址
  12. 测试用户有 已经通完认证 的属性,完成后进行功能测试
  13. 多个测试来测试用户退出,用户在登录状态,用户点退出按钮,用户退出,刷新后还在退出状态
    一个回滚指令把15和16章都删除了,要慎重,或者新开一个分支操作
uv run manage.py shell  
from django.contrib.sessions.models import Session  
session = Session.objects.get(session_key="w9wpdnbwouwxtpznyjmfyv9xdg0l1u48")  
print(session.get_decoded())  
{'_auth_user_id': '1', '_auth_user_backend': 'accounts.views.EmailBackend', '_auth_user_hash': '58856ebbb2e795f3f8759e7b6e69af2ec4bbd9584fbb129b48416102b9c35e6e'}  

17

  1. 在功能测试里建一个list测试,用事先创建好会话,跳过登录过程
  2. 移植的wait开头函数, id,登录,登出,需要可以接收任意邮件地址
  3. 检测用户发现自己已经登录
  4. 写一个Gunicorn的配置,不一定要用,但是有,保留访问和错误日志,及对应setting配置
  5. 在功能测试里面建一个管理模块,模块下面有一个命令模块,在下面建一个建立session的测试
  6. 把功能测试加入settings,并测试
  7. 调整 test_my_lists 使之可以在非本地运行,在base中设定 self.against_staging
  8. 使用subprocess实现,在远端服务器建立session及测试完还原远端数据库,不要测试,仅写代码,或者数据库路径,获取manage文件
  9. 为日志行为编写测试 accounts/tests/test_authentication 里面增加日志输出

ALLOWED_HOSTS 配置

18

  1. 建立多个功能测试,实现我的list:已登录用户查看保存的list,已登录用户,访问首页,建立list,my list链接,以创建的第一个代办事项命名,再建个清单,my list页也显示出了新的清单,退出出 my list链接消失
  2. 建立view测试,测试我的list链接renders我的链接的模版
  3. 再建view测试,list属主有被记录,如果用户是通过认证的
  4. 建立模型测试,list能拥有属主,另一个测试list的属主可选
  5. 模型测试,list的名字是第一个item的文字

19

  1. 建一个新分支,暂时屏蔽清单的.owner 属性,重跑view测试,list属主有被记录,如果用户是通过认证的,失败先不要修复(下面的任务也是)
  2. 改造上个任务的测试,用模拟的方式让视图层认为清单有属主,mock_list ,测试后在测试里面建一个子函数,检查属主的指定,定义side_effect,完成测试
  3. 重构测试,把任务交给表单,
  4. 把NewListTest类重命名为NewListViewIntegratedTest,把尝试使用驭件保存属性的测试代码删掉,换成整合版本,并暂时skip
  5. 从头编写测试,完全隔离,看隔离测试能否驱动写出new_list 视图的替代版本,命名为new_list2
  6. 使用驭件模拟表单,使用 unittest.TestCase, 使用new_list2的 test_views,setUp,test_passes_POST_data_to_NewListForm
  7. 视图测试,test_redirects_to_form_returned_object_if_form_valid
  8. test_renders_home_template_with_form_if_form_invalid
  9. test_does_not_save_if_form_invalid
  10. test_save_creates_new_list_and_item_form_post_data, 子函数check_item_text_and_list
  11. test_save_creates_new_list_from_post_data_if_user_not_authenticated
  12. test_save_creates_new_list_from_post_data_if_user_authenticated
  13. 模型层写整合测试,test_get_absoluted_url, test_create_new_creates_list_and_first_item,
  14. test_create_new_optionally_saves_owner
  15. 换掉之前的视图,用新视图试试,删除掉之间的skip标记,使用new_list2测试,
  16. 更新视图测试,test_redirects_to_form_returned_object_if_form_valid,使用mock_form.save.return_value
  17. 审查视图测试中的每个测试,是否忽视了隐含假设,
  18. 新增表单测试,确保表单返回刚保存的清单,尝试修复先前各项失败的测试,
  19. 使用模型测试,test_list_name_is_first_item_text
  20. 清理多余测试:表单层save方法的测试,之间的 new_list 视图,并把new_list2改名回去,其他一些视图层的测试(保留 test保存一个post请求,test失效的输入不保存但报错,test保存list的owner如果用户已登录),
  21. 测试成功后,代码合并回主分支
    什么时候编写隔离测试,什么时候编写整合测试?(Django Test Client(不启动真实浏览器))
    功能测试,整合测试,隔离测试(不是太懂),解耦应用代码和ORM代码(Item.objects,目前没解)

原始问题

  • 过度依赖数据库 - 视图测试每次都访问数据库
  • 测试脆弱 - 改一个字段导致大量测试失败
  • 耦合度高 - 视图、表单、模型紧密耦合
  • 难以定位错误 - 整合测试无法快速定位问题在哪一层

21

  1. 新建多个功能测试:有2个用户使用addCleanup,a已登录,b也在使用,a访问首页,新建清单,然后看到分享清单选项,填写b的邮件地址,再点分享按钮
  2. 拆封功能测试的辅助代码,使用页面模式,定义list_page类,新建清单,分享清单后页面更新,获取分享框,获取list分享,分享list,
  3. 功能测试,b在访问清单,b看到了a分享的清单,
  4. 到我的list页面,
  5. b看到了a的清单,b在清单上加了一个项目,a刷新页面看到了新增项目

前言  xv
准备工作和应具备的知识  xxi
致谢  xxvii
第一部分 TDD 和Django 基础
第1章 使用功能测试协助安装Django  3
1.1 遵从测试山羊的教诲,没有测试什么也别做  3
1.2 让Django运行起来  6
1.3 创建Git仓库  7
第2章 使用unittest模块扩展功能测试  11
2.1 使用功能测试驱动开发一个最简可用的应用  11
2.2 Python标准库中的unittest模块  14
2.3 隐式等待  16
2.4 提交  16
第3章 使用单元测试测试简单的首页  18
3.1 第一个Django应用,第一个单元测试  19
3.2 单元测试及其与功能测试的区别  19
3.3 Django中的单元测试  20
3.4 Django中的MVC、URL 和视图函数  21
3.5 终于可以编写一些应用代码了  22
3.6 urls.py  24
3.7 为视图编写单元测试  27
第4章 编写这些测试有什么用  31
4.1 编程就像从井里打水  31
4.2 使用Selenium测试用户交互  33
4.3 遵守“不测试常量”规则,使用模板解决这个问题  35
4.4 关于重构  39
4.5 接着修改首页  40
4.6 总结:TDD流程  42

05 保存用户输入  45

5.1 编写表单,发送POST请求  45
5.2 在服务器中处理POST请求  48
5.3 把Python变量传入模板中渲染  49
5.4 事不过三,三则重构  53
5.5 Django ORM和第一个模型  54
 第一个数据库迁移  56
 测试向前走得挺远  57
 添加新字段就要创建新迁移  57
5.6 把POST请求中的数据存入数据库  58
5.7 处理完POST请求后重定向  61
5.8 在模板中渲染待办事项  63
5.9 使用迁移创建生产数据库  65

06 完成最简可用的网站 

6.1 确保功能测试之间相互隔离  70
6.2 必要时做少量的设计  74
 YAGNI  74
 REST  75
6.3 使用TDD 实现新设计  76
6.4 逐步迭代,实现新设计  78
6.5 使用Django测试客户端一起测试视图、模板和URL  80
 一个新测试类  80
 一个新URL  81
 一个新视图函数  81
 一个新模板,用于查看清单  82
6.6 用于添加待办事项的URL和视图  85
 用来测试新建清单的测试类  85
 用于新建清单的URL和视图  86
 删除当前多余的代码和测试  88
 让表单指向刚添加的新URL  88
6.7 调整模型  89
 通过外键实现的关联  91
 根据新模型定义调整其他代码  92
6.8 每个列表都应该有自己的URL  94
 捕获URL中的参数  95
 按照新设计调整new_list视图  96
6.9 还需要一个视图,把待办事项加入现有清单  97
 小心霸道的正则表达式  98
 最后一个新URL  98
 最后一个新视图  99
 如何在表单中使用那个URL  100
6.10 使用URL 引入做最后一次重构  102
第二部分 Web开发要素

07 美化网站:布局、样式及其测试方法

7.1 如何在功能测试中测试布局和样式  106
7.2 使用CSS框架美化网站  109
7.3 Django模板继承  111
7.4 集成Bootstrap  112
7.5 Django中的静态文件  114
7.6 使用Bootstrap中的组件改进网站外观  116
 超大文本块  116
 大型输入框  116
 样式化表格  117
7.7 使用自己编写的CSS  117
7.8 补遗:collectstatic命令和其他静态目录  118
7.9 没谈到的话题  121

08 使用过渡网站测试部署  不操作

8.1 TDD以及部署的危险区域  123
8.2 一如既往,先写测试  124
8.3 注册域名  126
8.4 手动配置托管网站的服务器  126
 选择在哪里托管网站  127
 搭建服务器  127
 用户账户、SSH和权限  128
 安装Nginx  128
 解析过渡环境和线上环境所用的域名  129
 使用功能测试确认域名可用而且Nginx正在运行  130
8.5 手动部署代码  130
 调整数据库的位置  131
 创建虚拟环境  133
 简单配置Nginx  135
 使用迁移创建数据库  137
8.6 为部署到生产环境做好准备  138
 换用Gunicorn  138
 让Nginx伺服静态文件  139
 换用Unix套接字  140
 把DEBUG设为False,设置ALLOWED_HOSTS  141
 使用Upstart确保引导时启动Gunicorn  141
 保存改动:把Gunicorn 添加到requirements.txt  142
8.7 自动化  143

09 使用Fabric自动部署  不操作

9.1 分析一个Fabric部署脚本  148
9.2 试用部署脚本  151
 部署到线上服务器  153
 使用sed配置Nginx 和Gunicorn  155
9.3 使用Git标签标注发布状态  155
9.4 延伸阅读  156

10 输入验证和测试的组织方式

10.1 针对验证的功能测试:避免提交空待办事项  158
 跳过测试  159
 把功能测试分拆到多个文件中  160
 运行单个测试文件  162
 填充功能测试  163
10.2 使用模型层验证  164
 重构单元测试,分拆成多个文件  164
 模型验证的单元测试和self.assertRaises上下文管理器  165
 Django怪异的表现:保存时不验证数据  166
10.3 在视图中显示模型验证错误  167
10.4 Django模式:在渲染表单的视图中处理POST 请求  171
 重构:把new_item实现的功能移到view_list 中  172
 在view_list 视图中执行模型验证  174
10.5 重构:去除硬编码的URL  176
 模板标签{% url %}  176
 重定向时使用get_absolute_url  177

11 简单的表单  

11.1 把验证逻辑移到表单中  181
 使用单元测试探索表单API  182
 换用Django中的ModelForm类  183
 测试和定制表单验证  184
11.2 在视图中使用这个表单  186
 在处理GET请求的视图中使用这个表单  187
 大量查找和替换  189
11.3 在处理POST请求的视图中使用这个表单  191
 修改new_list视图的单元测试  191
 在视图中使用这个表单  192
 使用这个表单在模板中显示错误消息  193
11.4 在其他视图中使用这个表单  194
11.5 使用表单自带的save方法  196

12 高级表单  199

12.1 针对重复待办事项的功能测试  199
 在模型层禁止重复  200
 题外话:查询集合排序和字符串表示形式  202
 重写旧模型测试  204
 保存时确实会显示完整性错误  205
12.2 在视图层试验待办事项重复验证  206
12.3 处理唯一性验证的复杂表单  207
12.4 在清单视图中使用ExistingListItemForm  209

13 试探JavaScript 

13.1 从功能测试开始  213
13.2 安装一个基本的JavaScript 测试运行程序  214
13.3 使用jQuery 和<div>固件元素  217
13.4 为想要实现的功能编写JavaScript单元测试  219
13.5 JavaScript测试在TDD循环中的位置  221
13.6 经验做法:onload样板代码和命名空间  222
13.7 一些缺憾  223
第14章 部署新代码  224
14.1 部署到过渡服务器  224
14.2 部署到线上服务器  225
14.3 如果看到数据库错误该怎么办  225
14.4 总结:为这次新发布打上Git 标签  225
第三部分 高级话题

15 用户认证、集成第三方插件以及JavaScript模拟技术的使用

15.1 Mozilla Persona(BrowserID)  229
15.2 探索性编程(又名“探究”)  229
 为此次探究新建一个分支  230
 前端和JavaScript代码  230
 Browser-ID协议  231
 服务器端:自定义认证机制  232
15.3 去掉探究代码  237
 常用Selenium技术:显式等待  240
 删除探究代码  241
15.4 涉及外部组件的JavaScript单元测试:首次使用模拟技术  242
 整理:全站共用的静态文件夹  242
 什么是模拟技术,为什么要模拟,模拟什么  244
 命名空间  244
 在initialize函数的单元测试中使用一个简单的驭件  245
 高级模拟技术  250
 检查参数的调用  253
 QUnit 中的setup和teardown函数,以及Ajax请求测试  255
 深层嵌套回调函数和测试异步代码  259

16 服务器端认证,在Python中使用模拟技术  262

16.1 探究登录视图  262
16.2 在Python代码中使用模拟技术  263
 通过模拟authenticate函数测试视图  263
 确认视图确实登录了用户  266
16.3 模拟网络请求,去除自定义认证后台中的探究代码  270
 一个if 语句需要一个测试  270
 在类上使用patch修饰器  272
 进行布尔值比较时要留意驭件  275
 需要时创建用户  276
 get_user方法  277
16.4 一个最简单的自定义用户模型  279
 稍微有点儿失望  280
 把测试当作文档  281
 用户已经通过认证  282
16.5 关键时刻:功能测试能通过吗  283
16.6 完善功能测试,测试退出功能  284

17 测试固件、日志和服务器端调试 

17.1 事先创建好会话,跳过登录过程  287
17.2 实践是检验真理的唯一标准:在过渡服务器中捕获最后的问题  290
 设置日志  291
 修正Persona引起的这个问题  293
17.3 在过渡服务器中管理测试数据库  295
 创建会话的Django管理命令  295
 让功能测试在服务器上运行管理命令  296
 使用subprocess模块完成额外的工作  298
17.4 集成日志相关的代码  301
17.5 小结  304

18 完成“My Lists”页面:由外而内的TDD  

18.1 对立技术:“由内而外”  305
18.2 为什么选择使用“由外而内”  306
18.3 “My Lists”页面的功能测试  306
18.4 外层:表现层和模板  307
18.5 下移一层到视图函数(控制器)  308
18.6 使用由外而内技术,再让一个测试通过  309
 快速重组模板的继承层级  309
 使用模板设计API  310
 移到下一层:视图向模板中传入什么  311
18.7 视图层的下一个需求:新建清单时应该记录属主  312
18.8 下移到模型层  313

19 测试隔离和“倾听测试的心声”  

19.1 重温抉择时刻:视图层依赖于尚未编写的模型代码  318
19.2 首先尝试使用驭件实现隔离  319
19.3 倾听测试的心声:丑陋的测试表明需要重构  322
19.4 以完全隔离的方式重写视图测试  323
 为了新测试的健全性,保留之前的整合测试组件  323
 完全隔离的新测试组件  324
 站在协作者的角度思考问题  324
19.5 下移到表单层  328
19.6 下移到模型层  332
19.7 关键时刻,以及使用模拟技术的风险  335
19.8 把层与层之间的交互当作“合约”  336
 找出隐形合约  337
 修正由于疏忽导致的问题  338
19.9 还缺一个测试  340
19.10 清理:保留哪些整合测试  340
 删除表单层多余的代码  340
 删除以前实现的视图  341
 删除视图层多余的代码  342
19.11 总结:什么时候编写隔离测试,什么时候编写整合测试  343
 以复杂度为准则  344
 两种测试都要写吗  344
 继续前行  345
第20章 持续集成  346
20.1 安装Jenkins  346
 Jenkins的安全配置  348
 添加需要的插件  348
20.2 设置项目  350
20.3 第一次构建  351
20.4 设置虚拟显示器,让功能测试能在无界面的环境中运行  353
20.5 截图  355
20.6 一个常见的Selenium问题:条件竞争  358
20.7 使用PhantomJS运行QUnit JavaScript测试  361
 安装node  361
 在Jenkins中添加构建步骤  362
20.8 CI 服务器能完成的其他操作  364

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

21.1 有多个用户以及使用addCleanup 的功能测试  365
21.2 实现Selenium交互等待模式  367
21.3 页面模式  368
21.4 扩展功能测试测试第二个用户和“My Lists”页面  371
21.5 留给读者的练习  373
第22章 测试运行速度的快慢和炽热的岩浆  375
22.1 正题:单元测试除了运行速度超快之外还有其他优势  376
 测试运行得越快,开发速度越快  376
 神赐的心流状态  377
 速度慢的测试经常不想运行,导致代码变坏  377
 现在还行,不过随着时间推移,整合测试会变得越来越慢  377
 别只听我一个人说  377
 单元测试能驱使我们实现好的设计  377
22.2 纯粹的单元测试有什么问题  378
 隔离的测试难读也难写  378
 隔离测试不会自动测试集成情况  378
 单元测试几乎不能捕获意料之外的问题  378
 使用驭件的测试可能和实现方式联系紧密  378
 这些问题都可以解决  379
22.3 合题:我们到底想从测试中得到什么  379
 正确性  379
 简洁可维护的代码  379
 高效的工作流程  379
 根据所需的优势评估测试  379
22.4 架构方案  380
 端口和适配器(或六边形、简洁)架构  380
 函数式核心,命令式外壳  381
22.5 小结  381
遵从测试山羊的教诲  383
附录A PythonAnywhere  385
附录B 基于类的Django 视图  388
附录C 使用Ansible配置服务器  398
附录D 测试数据库迁移  402
附录E 接下来做什么  407
附录F 速查表  411
附录G 参考书目  415
作者简介  416
封面介绍  416

解释一下前序的工作,使用表单,或ModelForm,和最初的做法有什么区别,这样做有什么好处

● 表单/ModelForm vs 最初做法的对比

最初的做法(没有表单)

views.py

def home_page(request):
if request.method == 'POST':
list_ = List.objects.create()
# ❌ 直接访问POST数据,没有验证
Item.objects.create(
text=request.POST['item_text'], # 危险!
list=list_
)
return redirect(f'/lists/{list_.id}/')
return render(request, 'home.html')

问题:

  • ❌ 没有验证空值
  • ❌ 没有错误处理
  • ❌ 直接操作 POST 数据(不安全)
  • ❌ 验证逻辑散落在各处

使用 ModelForm 后

forms.py

class ItemForm(forms.ModelForm):
item_text = forms.CharField(
error_messages={'required': EMPTY_LIST_ERROR}
)
class Meta:
model = Item
fields = ('text',)

views.py

def home_page(request):
form = ItemForm()
if request.method == 'POST':
form = ItemForm(data=request.POST) # ✅ 自动验证
if form.is_valid(): # ✅ 检查验证
list_ = List.objects.create()
form.save(for_list=list_) # ✅ 安全保存
return redirect(list_)
return render(request, 'home.html', {'form': form})

优势:

  • ✅ 自动验证(空值、长度等)
  • ✅ 集中管理验证规则
  • ✅ 自动生成HTML
  • ✅ 安全的数据处理

关键区别对比表
┌──────────┬───────────────────────────────┬───────────────────────────────┐
│ 方面 │ 最初做法 │ 使用表单后 │
├──────────┼───────────────────────────────┼───────────────────────────────┤
│ 验证 │ 在模型 save() 中 full_clean() │ 表单自动验证 is_valid() │
├──────────┼───────────────────────────────┼───────────────────────────────┤
│ 错误处理 │ 捕获 ValidationError │ form.errors 自动收集 │
├──────────┼───────────────────────────────┼───────────────────────────────┤
│ HTML生成 │ 手写 标签 │ {{ form.item_text }} 自动生成 │
├──────────┼───────────────────────────────┼───────────────────────────────┤
│ 代码位置 │ 验证逻辑在视图 │ 验证逻辑在表单 │
├──────────┼───────────────────────────────┼───────────────────────────────┤
│ 可重用性 │ 无法重用 │ 表单可在多处使用 │
├──────────┼───────────────────────────────┼───────────────────────────────┤
│ 安全性 │ 直接访问 POST 数据 │ 表单自动清理数据 │
└──────────┴───────────────────────────────┴───────────────────────────────┘
三大核心好处

  1. 关注点分离 (Separation of Concerns)

❌ 之前:视图做了太多事

def home_page(request):
if request.method == 'POST':
# 视图要处理:验证、错误、保存...
try:
item = Item(text=request.POST['item_text'])
item.full_clean() # 验证
item.save()
except ValidationError:
error = "..." # 错误处理
return render(...)

✅ 现在:各司其职

forms.py - 表单负责验证

class ItemForm(forms.ModelForm):
item_text = forms.CharField(...)

views.py - 视图负责流程控制

def home_page(request):
if form.is_valid():
form.save()

  1. DRY 原则

❌ 之前:重复的验证代码

def home_page(request):
if request.method == 'POST':
if not request.POST['item_text']:
return error
# ...

def view_list(request, list_id):
if request.method == 'POST':
if not request.POST['item_text']:
return error # 重复!
# ...

✅ 现在:验证逻辑只写一次

class ItemForm(forms.ModelForm):
item_text = forms.CharField(
required=True, # 一次定义
error_messages={...}
)

  1. 可测试性

❌ 之前:很难单独测试验证逻辑

必须通过整个视图测试

✅ 现在:可以独立测试表单

class ItemFormTest(TestCase):
def test_form_validation_for_blank_items(self):
form = ItemForm(data={'item_text': ''})
self.assertFalse(form.is_valid())
self.assertEqual(form.errors['item_text'], [EMPTY_LIST_ERROR])

数据流程对比

最初做法

用户输入 → POST数据 → 视图直接访问 → 模型save()

full_clean() 验证

ValidationError(可能崩溃)

使用表单后

用户输入 → POST数据 → 表单验证 → is_valid()

True → form.save() → 模型
False → form.errors → 显示错误

总结

使用 ModelForm 的本质是:将验证逻辑从业务代码中剥离出来,交给框架处理。

这就像:

  • 之前:自己手写门锁,每次开门都要检查锁芯
  • 现在:用工厂的锁,验证由工厂保证,你只管开门

这样代码更简洁、更安全、更易维护!

最后写了个skill

如何在Plan中描述TDD节奏

你可以这样描述:

步骤1:添加第一个失败测试

目标: 写一个测试来验证POST保存功能
操作:

  • 在 tests.py 添加 test_home_page_can_save_a_post_request
    验证: 运行 python manage.py test (预期:红灯❌)
    ⚠️ 重要:看到红灯后才能继续下一步

步骤2:让第一个测试通过

目标: 修改view,只做最少的改动让测试通过
操作:

  • 修改 views.py,添加简单的保存逻辑
    验证: 运行 python manage.py test (预期:绿灯✅)
    ⚠️ 重要:测试通过后立即停止,不要继续优化

步骤3:重构(如果需要)

目标: 清理代码,但不改变行为
验证: 运行测试确保没有破坏功能

关键改进点

  1. 明确每个步骤的验证点
    验证步骤: 运行测试并确认结果
    预期状态: 🔴 红灯 / 🟢 绿灯
  2. 强调"一次只做一件事"
    限制:
  • 只修改测试中提到的文件
  • 只写足够让测试通过的最少代码
  • 不要提前实现后续功能
  1. 添加检查点
    检查点:
  • 测试是否失败?
  • 是否只修改了必要的代码?
  • 测试通过后是否停止了?
  1. 使用"原子化"的步骤描述

步骤X:添加测试Y

  • 只写测试
  • 运行测试确认失败
  • 截图/记录错误信息

步骤X+1:实现Y

  • 只写实现代码
  • 运行测试确认通过
  • 立即停止,不继续

建议的Plan模板

功能实现计划(TDD方式)

循环1:基础保存功能

1.1 编写测试(红灯阶段)

  • 添加测试:test_can_save_post
  • 运行测试:python manage.py test
  • 确认看到失败输出
  • 记录失败原因:_______

1.2 实现功能(绿灯阶段)

  • 修改views.py,只添加保存逻辑
  • 运行测试:python manage.py test
  • 确认测试通过
  • ⚠️ 停止!不要继续优化

1.3 重构(可选)

  • 检查代码质量
  • 运行测试确认没有破坏

循环2:重定向功能

重复上述模式...


最终验证

  • 所有单元测试通过
  • 功能测试通过
  • 手动验证

给AI的明确指令

在plan开头加上:

TDD执行规则

  1. 严格顺序:必须按步骤顺序执行,不能跳过
  2. 验证门控:每步必须验证测试状态(红/绿)后才能继续
  3. 最小改动:每次只写最少的代码让当前测试通过
  4. 立即停止:测试通过后立即停止,不继续实现其他功能
  5. 单步确认:每个步骤完成后向用户确认结果

这样AI就会理解:这不是一个"实现所有功能"的任务,而是"一步步迭代"的过程。

About

django tdd

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors