还是挺厉害的,上一次做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
- 新建一个测试,调用selenium 访问LiveServerUrl,窗口大小1024x768,需要输入框完美居中显示,并运行该测试,
- 继续上一个测试,输入清单testing,输入框仍然完美居中显示
- 给项目安装并配置Bootstrap
- 使用模板继承,建立base.html,再次运行测试
- 在base.html中加入Bootstrap,重写输入框,运行测试
- 定义 /static/ 前缀是静态资源,运行测试
- 换用StaticLiveServerTestCase 运行测试x
- Django 5.x移除了StaticLiveServerTestCase
- 使用Bootstrap中的超大文本块,大型输入框,样式化表格
- 任务8 使用Bootstrap的超大文本块(jumbotron没有使用)、大型输入框(input-lg同样)、样式化表格(table类) ,因为版本移除改变了写法
- 自己编写css,让输入表单离标题文字远一点,运行测试
- 使用collectstatic命令把静态文件移到项目之外,不进git仓库
- 输入完结果显示在下方应该也是居中的,并且前面有数字冒号空格,不知道是哪里遗漏了,帮忙补上测试再撰写代码
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
- 使用单元测试探索表单API, ItemForm, form.as_p,并测试
- 写一个单元测试检查placeholder属性和css类
- 换用Django中的ModelForm类
- 测试ModelForm是否应用了模型中定义的验证规则(空提交,is_valid函数)
- 使用 EMPTY_LIST_ERROR
- 在处理GET请求的视图中使用这个表单, 修改视图的单元测试,使用测试客户端编写新测试,替代 home_page_returns_rorrect_html,和 test_root_url_resolves_to_home_page_view,旧测试先不删除
1. 在第6项任务中,我发现 lists/templates/base.html 中没有使用input也没有换成 form.text, 是漏掉了,还是版本原因,这块是怎么实现的 - 测试通过后,替换旧测试的相关位置(例如 id_new_item)
- 在base.py 添加一个辅助方法,在需要替换的地方使用辅助方法
- 在处理POST请求的视图中使用这个表单,测试NewListTest,修改new_list 试图的单元测试(有验证错误,渲染首页,返回200,响应包含错误,表单对象传入模板,不用硬编码,用常量
- 在视图中使用这个表单,在模板中显示错误消息
- 写测试view_list 处理get表单请求,post请求,辅助方法 post_invalid_input,表单提交失败测试拆分,post,存入数据库,渲染,pass,显示的报错,并测试,
1. 漏掉了辅助方法和数据库没修改的测试 - 测试保存handles 到 list,更新保存方法,并用instance指定
- 重构视图
12
- 建立针对重复事项的功能测试,可以输入“buy sth”,成功后再输入“buy sth”,的形式来测试 ,对这种情况需要弹出错误提示
- 在模型层建立同样情况测试,禁止重复 ,
- 同样位置再建立个测试,同样内容可以存入不同list中
- 使用Meta类在models模块实现单个list中事件唯一的约束
- 测试list中事项排序,可能查询集合要转成列表才能比较
- 重写旧模型测试,删除test_saving_and_retrieving_items,或类似测试,换成 test_default_text,test_item_is_related_to_list, 并改成针对Item和List的测试
- 测试保存时显示完整性错误, 在模块测试,检查重复事项的测试里,更新出保存事项,如果报错就改回去
- 在视图层检测待办事项重复验证,如果检测到,暂时skip跳过,不修复
- ExistingListItemForm测试,分别检查渲染输入项,验证空的项,重复字段的项
- 在清单视图中使用ExistingListItemForm, 去掉任务8加的skip标记,使用任务9建立的测试,修复view测试 test_display_item_form, test_for_invalid_input_passes_form_to_template
- 编写单元测试,test_form_save,
models.py 有一些差异,但claude说,不是当前任务的需求造成的
确实 test_duplicate_item_validation_errors_and_up_on_lists_pages claude说其他测试功能包含
13
- 新建功能测试,新建清单不当,出现验证错误,在输入框输入内容,错误消失
- 多次使用CSS查找错误消息元素,所以建了个辅助函数,并应用起来
- 在 lists/static/tests 下,布署Qunit,并生成一个tests.html,里面放一个冒烟测试
- 在tests.html中加入jQuery:错误可见,隐藏错误,错误不可见
- 使用 qunit-fixture 来帮助完成测试
- 代码命名为 list.js 放在单独的js中,测试
- 在所有页面引用脚本和jQuery
- 添加onload样板代码和命名空间
js的测试确实不太好做,或者说不熟悉
15
建一个测试分支
- 探究代码, 在lists/templates/base.html 中加入Supabase Auth代码,并把navigator.id 的 request方法绑到登录链接上
- 启动django的账户系统应用,处理 post请求 accounts/views.py 及认证代码,自定义用户模型,模型管理器,退出视图,URL 映射
- 新建功能测试,访问,登录,出现登录框,使用邮箱地址登录,mockmyid.com,点击后,窗口关闭,登录成功,用到2个辅助函数,循环切到新窗口,显式等待,并测试
- 测试成功后,回到主分支,删除探究代码 accounts,用测试驱动开发
- 在superlists/superlists 中建全站共用静态文件目录,调用现有单元测试,
- 建accounts应用,及js的test目录,除取客户端 JavaScript中的探究代码,换Supabase 代码,
- 使用模拟技术,给lists/templates/base.html 中把navigator传入初始化函数
- 在initialize函数的单元测试中使用mock,确保requestWasCalled 初始值是 false
- 使用sinon.js 创建mock,辅助函数看当前用户
- 建立测试,initialize calls navigator.id.watch 之前加入module函数
- 重写前面的2个测试,测试onlogin回掉函数
- 建立测试 onlogin post failure should do navigator.id.logout
完成第12个任务后,如果 test functional_tests.test_login 失败,暂不处理
Persona 服务已经下线了,但测试中没有测出来,需要关注
16
- 在Python代码中使用模拟技术,删除默认 accounts/tests/ 目录,新建一个,新建 LoginViewTest 测试,通过模拟authenticate函数测试视图
- 加一个测试,找到用户就返回ok
- 加测试,如果认证反回用户,获取登录session
- 加测试,如果认证返回None,客户端不在登录状态
- 模拟网络请求,测试带着域名发送assertion 到 mozilla,检查响应,提取邮件地址,如果通过不了,模拟一个数据,继续下一项测试
- 测试,上一个测试访问响应失败或有错误则返回None,用和上一个相同的请求
- 测试响应json中的状态是否为okay
- 检测是否能通过email寻找到现有用户,也许需要setUp辅助函数,在测试前保证有一个用户
- 测试传入数据persona确认有效(模拟),但数据库没有用户,创建新用户
- 测试通过email查找用户和找不到用户返回none
- 测试模型,不保存用户姓和名只需要邮件地址
- 测试用户有 已经通完认证 的属性,完成后进行功能测试
- 多个测试来测试用户退出,用户在登录状态,用户点退出按钮,用户退出,刷新后还在退出状态
一个回滚指令把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
- 在功能测试里建一个list测试,用事先创建好会话,跳过登录过程
- 移植的wait开头函数, id,登录,登出,需要可以接收任意邮件地址
- 检测用户发现自己已经登录
- 写一个Gunicorn的配置,不一定要用,但是有,保留访问和错误日志,及对应setting配置
- 在功能测试里面建一个管理模块,模块下面有一个命令模块,在下面建一个建立session的测试
- 把功能测试加入settings,并测试
- 调整 test_my_lists 使之可以在非本地运行,在base中设定 self.against_staging
- 使用subprocess实现,在远端服务器建立session及测试完还原远端数据库,不要测试,仅写代码,或者数据库路径,获取manage文件
- 为日志行为编写测试 accounts/tests/test_authentication 里面增加日志输出
ALLOWED_HOSTS 配置
18
- 建立多个功能测试,实现我的list:已登录用户查看保存的list,已登录用户,访问首页,建立list,my list链接,以创建的第一个代办事项命名,再建个清单,my list页也显示出了新的清单,退出出 my list链接消失
- 建立view测试,测试我的list链接renders我的链接的模版
- 再建view测试,list属主有被记录,如果用户是通过认证的
- 建立模型测试,list能拥有属主,另一个测试list的属主可选
- 模型测试,list的名字是第一个item的文字
19
- 建一个新分支,暂时屏蔽清单的.owner 属性,重跑view测试,list属主有被记录,如果用户是通过认证的,失败先不要修复(下面的任务也是)
- 改造上个任务的测试,用模拟的方式让视图层认为清单有属主,mock_list ,测试后在测试里面建一个子函数,检查属主的指定,定义side_effect,完成测试
- 重构测试,把任务交给表单,
- 把NewListTest类重命名为NewListViewIntegratedTest,把尝试使用驭件保存属性的测试代码删掉,换成整合版本,并暂时skip
- 从头编写测试,完全隔离,看隔离测试能否驱动写出new_list 视图的替代版本,命名为new_list2
- 使用驭件模拟表单,使用 unittest.TestCase, 使用new_list2的 test_views,setUp,test_passes_POST_data_to_NewListForm
- 视图测试,test_redirects_to_form_returned_object_if_form_valid
- test_renders_home_template_with_form_if_form_invalid
- test_does_not_save_if_form_invalid
- test_save_creates_new_list_and_item_form_post_data, 子函数check_item_text_and_list
- test_save_creates_new_list_from_post_data_if_user_not_authenticated
- test_save_creates_new_list_from_post_data_if_user_authenticated
- 模型层写整合测试,test_get_absoluted_url, test_create_new_creates_list_and_first_item,
- test_create_new_optionally_saves_owner
- 换掉之前的视图,用新视图试试,删除掉之间的skip标记,使用new_list2测试,
- 更新视图测试,test_redirects_to_form_returned_object_if_form_valid,使用mock_form.save.return_value
- 审查视图测试中的每个测试,是否忽视了隐含假设,
- 新增表单测试,确保表单返回刚保存的清单,尝试修复先前各项失败的测试,
- 使用模型测试,test_list_name_is_first_item_text
- 清理多余测试:表单层save方法的测试,之间的 new_list 视图,并把new_list2改名回去,其他一些视图层的测试(保留 test保存一个post请求,test失效的输入不保存但报错,test保存list的owner如果用户已登录),
- 测试成功后,代码合并回主分支
什么时候编写隔离测试,什么时候编写整合测试?(Django Test Client(不启动真实浏览器))
功能测试,整合测试,隔离测试(不是太懂),解耦应用代码和ORM代码(Item.objects,目前没解)
原始问题
- 过度依赖数据库 - 视图测试每次都访问数据库
- 测试脆弱 - 改一个字段导致大量测试失败
- 耦合度高 - 视图、表单、模型紧密耦合
- 难以定位错误 - 整合测试无法快速定位问题在哪一层
21
- 新建多个功能测试:有2个用户使用addCleanup,a已登录,b也在使用,a访问首页,新建清单,然后看到分享清单选项,填写b的邮件地址,再点分享按钮
- 拆封功能测试的辅助代码,使用页面模式,定义list_page类,新建清单,分享清单后页面更新,获取分享框,获取list分享,分享list,
- 功能测试,b在访问清单,b看到了a分享的清单,
- 到我的list页面,
- 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
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
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开发要素
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
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
9.1 分析一个Fabric部署脚本 148
9.2 试用部署脚本 151
部署到线上服务器 153
使用sed配置Nginx 和Gunicorn 155
9.3 使用Git标签标注发布状态 155
9.4 延伸阅读 156
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.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.1 针对重复待办事项的功能测试 199
在模型层禁止重复 200
题外话:查询集合排序和字符串表示形式 202
重写旧模型测试 204
保存时确实会显示完整性错误 205
12.2 在视图层试验待办事项重复验证 206
12.3 处理唯一性验证的复杂表单 207
12.4 在清单视图中使用ExistingListItemForm 209
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.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.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.1 事先创建好会话,跳过登录过程 287
17.2 实践是检验真理的唯一标准:在过渡服务器中捕获最后的问题 290
设置日志 291
修正Persona引起的这个问题 293
17.3 在过渡服务器中管理测试数据库 295
创建会话的Django管理命令 295
让功能测试在服务器上运行管理命令 296
使用subprocess模块完成额外的工作 298
17.4 集成日志相关的代码 301
17.5 小结 304
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.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.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 最初做法的对比
最初的做法(没有表单)
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 后
class ItemForm(forms.ModelForm):
item_text = forms.CharField(
error_messages={'required': EMPTY_LIST_ERROR}
)
class Meta:
model = Item
fields = ('text',)
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 数据 │ 表单自动清理数据 │
└──────────┴───────────────────────────────┴───────────────────────────────┘
三大核心好处
- 关注点分离 (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(...)
class ItemForm(forms.ModelForm):
item_text = forms.CharField(...)
def home_page(request):
if form.is_valid():
form.save()
- 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={...}
)
- 可测试性
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节奏
你可以这样描述:
目标: 写一个测试来验证POST保存功能
操作:
- 在 tests.py 添加
test_home_page_can_save_a_post_request
验证: 运行python manage.py test(预期:红灯❌)
⚠️ 重要:看到红灯后才能继续下一步
目标: 修改view,只做最少的改动让测试通过
操作:
- 修改 views.py,添加简单的保存逻辑
验证: 运行python manage.py test(预期:绿灯✅)
⚠️ 重要:测试通过后立即停止,不要继续优化
目标: 清理代码,但不改变行为
验证: 运行测试确保没有破坏功能
关键改进点
- 明确每个步骤的验证点
验证步骤: 运行测试并确认结果
预期状态: 🔴 红灯 / 🟢 绿灯 - 强调"一次只做一件事"
限制:
- 只修改测试中提到的文件
- 只写足够让测试通过的最少代码
- 不要提前实现后续功能
- 添加检查点
检查点:
- 测试是否失败?
- 是否只修改了必要的代码?
- 测试通过后是否停止了?
- 使用"原子化"的步骤描述
- 只写测试
- 运行测试确认失败
- 截图/记录错误信息
- 只写实现代码
- 运行测试确认通过
- 立即停止,不继续
建议的Plan模板
- 添加测试:
test_can_save_post - 运行测试:
python manage.py test - 确认看到失败输出
- 记录失败原因:_______
- 修改views.py,只添加保存逻辑
- 运行测试:
python manage.py test - 确认测试通过
-
⚠️ 停止!不要继续优化
- 检查代码质量
- 运行测试确认没有破坏
重复上述模式...
- 所有单元测试通过
- 功能测试通过
- 手动验证
给AI的明确指令
在plan开头加上:
- 严格顺序:必须按步骤顺序执行,不能跳过
- 验证门控:每步必须验证测试状态(红/绿)后才能继续
- 最小改动:每次只写最少的代码让当前测试通过
- 立即停止:测试通过后立即停止,不继续实现其他功能
- 单步确认:每个步骤完成后向用户确认结果
这样AI就会理解:这不是一个"实现所有功能"的任务,而是"一步步迭代"的过程。