前端测试用例怎么写?一看就懂

因此,每个开发都应该懂一些测试, 在日常的工作中多站在对方的角度思考问题,多体谅对方,将自己能力范围内的工作做好,问题解决好,为对方营造一个舒适的解决问题的环境,对方自然将你的好看在眼里,记在心里。
【1】搜索或查询
测试步骤 | 测试数据 | 预期结果 | 备注 |
单独遍历各查询条件,测试按各查询条件是否都能够查询出相应的值. | 查询出符合条件的记录 | ||
设置界面上所有查询条件进行查询,单击查询按钮后,测试执行查询操作后,查询条件是否能保留 | 能够执行查询,且已输入/选择的查询条件能够保留 | ||
随机对各查询条件进行组合查询,测试是否能够实现组合查询 | 各查询条件下存在相应的值 | 能够执行查询 | |
在各字符串类型字段中输入非精确值进行查询,测试是否能够实现糊模查询 | 各查询条件下存在相应的值 | 能够执行查询 | |
测试是否控制了各种非法字符的查询 | 输入@#$%^&*()<p>~'"%-like=?等符号 | 查询不到任何记录 | |
设置条件查询出记录后,翻到最后一页,再更改查询条件,测试各查询条件查询出记录后再切换条件查询是否正确。 | 第一个查询条件查询出来的记录页数必须多于第二个查询条件查询出来的记录页数 | 查询出相应的记录 | 第一次查询条件范围比第二次大,不一定说要存在分页 |
设置查询条件,点击查询按钮,测试未查询到任何记录时,是否会给予相应的提示 | 该查询条件下无任何记录 | 提示‘未查到任何记录,请放大查询条件试试!’ | |
设置查询条件,按回车键,测试是否支持回车查询 | 执行查询条件 | 可针对特定的场景不一定要实现 | |
对于英文字母是否区分大小写 | 不区分,通过大写字母Admin可以搜索到admin | ||
输入各查询条件,前中后包含空格 | 前后包含空格去掉空格查询,中间包含空格查不出记录 | ||
进行组合查询条件后,测试是否能够清空某一个查询条件 | 能够清空,每一个查询框都配有清空按钮 | ||
不输入或输入空格查询 | 提示请输入关键字查询 或 默认查询出所有记录 | ||
输入查询值,测试存在日期型查询字段时,查询是否正确 | 存在输入范围内的记录. | 能够执行查询 | |
输入查询值,测试当存在开始日期及结束日期进行查询 | 1、开始日期大于结束日期 | 1、给予提示信息。 | |
2、开始日期等于结束日期 | 2、能够正常查询。 | ||
3、开始日期小于结束日期,但两者格式不一 | 3、进行格式化后能够正常查询 | ||
超日期范围的查询 | 输入1890-01-01或2999-01-01进行查询 | 查询不到任何记录 | |
输入非完整的日期或其它数据,测试对日期型字段查询时,是否对非法数据进行了控制 | 如:2015-08~2015-09-30、205-02-02、2015-7-7、2015-07-或其他数据汉字、字母、特殊字符或随便输入数字 | 进行了格式化输入或给予明确提示 | |
输入不存在的日期进行查询 | 2015-02-30、2015-13-01、2015-07-32 | 给予提示信息 | |
输入不符合项目设定的日期类型,是否会做格式转换 | 如要求格式是2015-07-20,输入的是2015/07/20、2015.07.20等 | 自动转换或者不符合格式要求 | |
输入查询条件后,点重置 | 重置后,已输入或已选择的值都恢复为默认值 | ||
不输入或输入空格搜索 | 提示请输入关键字 |
【2】日期格式
测试步骤 | 测试数据 | 预期结果 |
输入正确的日期 | 如:2011-5-11 | 验证通过,输入正确 |
测试对日期数据的溢出是否进行了控制? | 1、输入符合要求的年月,输入32日,如1989-11-32 2、输入符合要求的年日,输入13月,如1989-13-12 3、输入符合要求的年,月输入4、6、9、11月,日输入31日 4、输入符合要求的非闰年,月输入2,日输入29,如2009-2-29 5、输入符合要求的闰年,月输入2,日输入30,如2008-2-30 | 给予提示信息,不允许提交 |
测试是否对日期型数据进行了格式化输入? | 1、输入不合法的日期,如2009-09、2009-09- 、200-2-2等 2、输入不符合该项目的日期格式,(如项目日期格式为2009-10-10,而输入的是2009/10/10、2009.10.10等) 3、随便输入数字,如1、22、456等 | 给出错误提示 |
测试是否对时间型数据是否进行了格式化输入? | 1、输入空白 2、输入汉字或字母 3、输入特殊字符串NULL、null、 空格的转义字符;<scrīpt></scrīpt>;<br>;<tr>;<td>;< /tr>等 | 进行了格式化输入,不可以输入非时间之外的数据 |
【3】输入框
测试步骤 | 测试数据 | 预期结果 |
输入中英文空格 | 如果必填的话:提示不能为空; | |
不输入任何字符或者输入空格 | 提示不能为空 | |
输入超长字符,超过边界值的,看页面显示 (不符合长度要求) | 大于或小于制定长度 | 给予提示信息 |
在各输入框中输入相应的值,测试输入框对空格的处理机制(空格) | 1、前面存在空格 2、后面存在空格 3、前/后都存在空格 4、中间存在空格 | 1、2、3能够正常去掉空格保存; 4 连同空格一起进行保存 |
【4】数字输入框
测试步骤 | 测试数据 | 预期结果 |
尝试输入字母,中文,标点或其他符号 | 提示输入字符不符合要求 | |
测试对输入框输入空格+数字 | 1、前面存在空格 2、后面存在空格 3、前/后都存在空格 4、中间存在空格 | 1、2、3能够正常去掉空格保存; 4 连同空格一起进行保存 |
按tab、空格键,测试输入框对快捷键的使用 | 光标能在输入框中来回切换 | |
测试0或非整数类型是否支持,是否符合业务逻辑 | 给与提示信息 |
【5】密码输入框
测试步骤 | 测试数据 | 预期结果 |
输入数字,测试密码是否转换成星号或其它符号 | 能将密码转换成星号或其他字符 | |
按tab键,测试输入框对快捷键的使用 | 光标能在输入框中来回切换 | |
若只允许输入字母,尝试输入数字;反之;尝试输入字母 | 提示输入字符不符合要求 | |
利用复制,粘贴等操作强制输入程序不允许的输入数据 | 给予提示信息 |
【6】上传图片(上传文件)浏览/选择按钮
测试步骤 | 测试数据 | 预期结果 |
文件类型正确,文件大小合适 | jpg或gif的格式图片,文件大小为4.9M | 上传成功 |
文件类型正确,文件大小不合适 | jpg或gif的格式图片,文件大小为5.1M | 上传的附件中大小不能超过5M |
文件类型正确,文件大小合适(临界值) | jpg或gif的格式图片,文件大小为5M | 上传成功 |
文件类型错误,文件大小合适的校验 | doc;.xls;ppt;bmp;jpeg;psd;tiff;tga;png;swf;svg;pcx;dxf;wmf;emf;lic;eps;.txt等格式文件,文件大小合适 | 提示“只能上传jpg或gif格式图片 |
文件类型和文件大小合法 | 上传一个0kb的图片 | 提示信息:“请重新上传文件,或者是不能上传0kb的图 |
文件类型和文件大小合法 | 上传一个正在使用中的图片(即打开该图片,在上传该图片) | 上传成功 |
文件类型和文件大小合法 | 手动输入一个存在的图片地址 | 上传成功 |
文件类型和文件大小合法 | 手动输入一个不存在的图片地址 | 提示:“请正确选择要上传的文件 |
文件类型和大小都合法 | 手动输入一个存在的图片名称 | 提示:“请正确选择要上传的文件的路径” |
连续多次选择不同的图片,查看是否上传最后一次选择的图片 | 多次选择图片,再点击“上传按钮” | 上传的是最后一次选择的图片 |
上一次上传失败后,再进行上传操作,测试上传失败后,是否还能继续上传 | 能够正常进行上传 |
【7】数据导入
测试步骤 | 测试数据 | 预期结果 | 备注 |
选择一个文件,点击上传按钮,测试是否能够完成数据导入功能? | 文件内的数据都符合格式 | 正常完成导入,导入成功后,给予明确的提示信息 | 对数据文件的导入,如果是Excel文件,存在多个sheet,是否可以导入 |
选择一个文件,点击上传按钮,测试导入文件中的数据异常,导入是否能正确判断? | 文件内的数据部分符合要求,部分不符合要求 | 检查完后,能够给予有多少不符合条件的记录提示,且可以点击链接查看具体不符合条件的记录;提供继续/终止两种选择;继续则忽略不符合条件记录继续导入;终止则停止本次导入,退出本次导入 | a、两个sheet内容一致; |
选择一个文件,点击上传按钮,测试导入文件中的数据异常,导入是否能正确判断? | 文件内的数据全部不符合要求 | 同上 | b、一个sheet内容符合要求,一个为空白; |
选择一个文件,点击上传按钮,测试导入文件中的数据异常,导入是否能正确判断? | 文件内的数据关键字段值在数据库中不存在 | 同上 | c、两个sheet内容不一致,个字段数据都是真实存在的; |
选择一个文件,点击上传按钮,测试导入文件中的数据异常,导入是否能正确判断? | 文件内的数据格式不符(如匹配字段名、表名等不同) | 同上 | 比如要求输入名字和手机号,文件内确是名字和身份证 |
【8】文件的导出或下载(考虑文件名内容是否有乱码)
测试步骤 | 测试数据 | 预期结果 | 备注 |
导出文件是否为空 | 看实际情况,若原本就没数据,导出可以为空,但是必须带各个字段名,数据可为空 | ||
导出文件名为特殊字符的情况 | 不该存在特殊字符 | 导出要采用多个浏览器检测,之前碰到在火狐浏览器正常,却在其他浏览器显示乱码 | |
导出全部资料的情况,导出的信息是否正确 | 信息要正确 | ||
导出部分资料的情况,导出的信息是否正确 | 信息要正确 | ||
导出大量数据时的时间是否在合理的时间范围内 | |||
导出目的磁盘空间已满的情况下,导出是否有友好的处理方式 | 可以给予友好的提示,存储空间不足等 | ||
导出目的的文件夹为只读的情况下,导出时是否有友好的的提示信息 | 先将目标文件夹状态改成只读形式 | 要提示 |
【9】分页
测试步骤 | 测试数据 | 预期结果 |
输入页数,点击go按钮,测试是否能执行翻页操作 | 正常转向所输入的页面,且当前页数及当前页的数据显示正确 | |
检测翻页功能栏的记录总条数统计是否正确 | 记录总条数统计正确 | |
点击首页、上一页、下一页、未页,测试各页面转换是否正常 | 正常进行转向,且当前页数及当前页数据显示正确 | |
测试在各边界面值再进行翻页是否正确 | 1、已在首页,点击首页或上一页; | 能够正常提示或不会报错,当前页数及当前页的数据显示正确 |
2、已在未页,点击未页或下一页; | ||
输入页数,点击go按钮,测试是否对所输入的页数进行了判断 | 1、输入非正整数 | 1、4、5限制不允许输入; |
2、输入超出的页数 | 2、3自动转到未页或给予提示 | |
3、输入超大的数值 | ||
4、输入字符、中文 | ||
5、输入特殊字符 | ||
对数据操作(增删)后是否正确显示数据 | 对分页列表中存在删除操作时,删除几条记录,页数是否更新 | 页数更新,信息正确显示 |
搜索后执行翻页,是否正确 | 正常 |
【10】全选
测试步骤 | 测试数据 | 预期结果 |
点击全选按钮,测试是否实现了全选当前页的功能 | 1、部分记录已被选中 2、全选前没有被选中的记录 | 当前页的全部记录被选中 |
点击全选按钮后,再点击全选按钮,测试全选按钮勾选状态变更是否正确 | 当前页的全部记录被取消选中 | |
点击全选按钮后,再去掉某些记录的勾选,测试全选按钮勾选状态变更是否正确 | 当前页的全选按钮取消选中 | |
将当前页的所有记录单个单个全部勾选,测试全选按钮勾选状态变更是否正确 | 当前页的全选按钮被选中 |
【11】删除
测试步骤 | 测试数据 | 预期结果 |
选择一条记录,执行删除操作,测试是否能正常进行删除操作 | 能够正常完成删除操作 | |
不选择任何记录,直接点击删除按钮,测试是否对无选择记录进行删除做了控制 | 提示‘请选择一条记录!’或删除按钮呈灰色显示或默认选择一条记录 | |
选择一条记录,点击删除按钮,测试删除时是否会给予相应的提示,并按所选项执行 | 提示‘您是否真的要删除所选中的记录?’,点击取消,不进行任何操作,回到原界面,点击确定删除,界面刷新不显示此条记录,停留在所删除记录所在页,总记录数-1 | |
设置条件查询出记录后翻页,选一条记录,点击【删除】并确定删除,测试删除边界值是否正常 | 翻页后,只存在一条记录 | 界面正常刷新至上一页,页数等于原页数-1。 |
勾选多条记录,点击删除按钮,在弹出的提示框中,点击确定,测试是否可以进行批量删除多条数据 | 能够正常批量删除多条数据 | |
选择记录进行删除,测试当删除相关连的信息时,是否给予明确的提示信息 | 1、所删除记录被其它引用,不允许被删除。 | 1、给予明确提示信息,不允许被删除(不给予确认删除的提示信息) |
2、所删除记录被其它引用,允许被删除。 | 2、给予明确提示信息,让用户确认是否删除,可选择是否继续,且被引用的信息也跟着删除掉了 |
【12】保存
测试步骤 | 测试数据 | 预期结果 |
进行新增或修改操作,输入相关内容后,多次提交保存,测试是否限制了重复保存 | 重复点击保存或 快速点击 | 在提交反馈之前,按钮呈灰色显示不允许点击 |
进行保存操作,测试当保存操作所费时间较长时,是否能提供进度条 | 当反馈时间达到12S以上时。 | 给予‘正在处理中’的进度条提示 |
【13】修改
测试步骤 | 测试数据 | 预期结果 | 备注 |
勾选一条记录,进行修改保存,测试是否能正常进行修改操作 | 能够正常进行修改保存,更新数据库中相应的值 | ||
未选择记录,点击修改按钮,测试是否对无选择记录进行修改做了控制 | 提示‘请选择一条记录!’或修改按钮呈灰色显示或默认选中第一条记录 | ||
选择一条记录,点击修改按钮,进行修改,点击确定按钮,测试是否对重复的记录进行了控制 | 将关键字段修改成一个已存在的值 | 提示‘该XX已存在,请重新输入 !’,光标停在XX输入框处 | |
选择一条记录,点击修改按钮,再点击返回按钮,测试取消修改时是否会给予提示 | 不修改任何内容 | 直接返回原记录所在页 | |
勾选多条记录,点击修改按钮,测试是否对勾选多个记录进行修改进行了控制。 | 提示‘您选择了多条记录,系统将修改第一条记录,且弹出第一条记录进行修改 | ||
修改引用的信息 | 引用的信息 | 提示不能修改,页面没有保存按钮 | |
选择一条记录进行修改保存,测试信息修改是否正常 | 该信息被其他模块引用 | 能够正常进行修改保存,且被引用的信息也跟着修改 | 例如员工信息里的公司名称,那么在公司信息里这个名称做了修改,员工信息里面包含公司名称的也全部做了修改,相反员工信息里面针对公司名称照道理不允许修改 |
【14】添加
测试步骤 | 测试数据 | 预期结果 | 备注 |
执行新增操作,测试是否能正常进行新增 | 输入必填字段 | 能正常完成新增操作 | |
只填写界面上标识的必填字段(即标识*号)号的字段,测试界面上必填字段控制与数据库必填控制是否一致 | 能够正常进行保存 | ||
输入相关数据后,进行保存,测试当因任何原因无法提交时,原输入的内容是否保存 | 制造提交失败的数据,比如某个字段输入不符合要求 | 提交失败后,界面的数据仍保留,不会清空 | |
执行新增操作,测试新增记录是否排在首行 | 对排序没有特殊要求的界面 | 满足新增记录排在首行的规则 | 不一定在首行,有些可能是按编号排序 |
执行新增操作,关键字段与数据库中已存在的值重复,测试是否对重复值进行了判断 | 重复提交以增加过的记录 | 提示该信息已经存在,请重新输入的提示信息 | |
执行新增操作,测试是否对各字段的长度进行了限制 | 各输入的值都大于表中定义的长度 | 在各输入框中,输入的内容达到所定义的长度时,则限制无法输入了。(如果输入的为中文字符,则控制到一半的长时就够了) | |
操作新增操作,测试是否对各输入框的非法字符进行了控制 | 在各输入框中,输入@#$%^&*()<p>~%[]-/’等。 | 1、不允许输入或提示‘你输入的 **中存在非法字符,请重新输入 2、允许输入保存后,能够正常回显 | |
在各输入框中输入相应的值进行新增保存,测试对各输入框中,输入内容前中后包含空格 | 1、前面存在空格 2、后面存在空格 3、前/后都存在空格 4、中间存在空格 | 1、2、3能够正常去掉空格保存; 4 连同空格一起进行保存
| |
在多行文本框中输入文字及回车进行保存,测试在多行文本框中,是否允许存在回车符 | 存在一个或多个回车键 | 能够正常进行保存及显示 | |
测试针对特殊字段限制是否正常 | 1、主页地址 2、电话号码 3、E-mail地址 | 1、主页地址中可输入\ 2、电话号码中可输入\- 3、E-mail地址中输入@ | 具体查看各个输入框对应校验,如电话号码 |
测试是否对必填字段进行了检查 | 关键字段不输入或输入空格 | 提示‘请输入XX,光标停留在第一个需输入的输入框处 | 光标离开输入框时便提示不符合标准 |
点击‘新增’按钮,在弹出的页面中,输入及选择相应信息,点击取消(返回)按钮,测试点击取消(返回)时是否会给予相应的提示 | 给予‘是否需要保存’的提示 | 需要这个提示比较好 | |
输入非负整型数据,测试是否对数值型数据进行了格式化输入 | 1、输入正常的正整数 2、输入0、负数、小数 3、输入超大值(超出该字段定义范围) 4、输入非数值(如数值+字符;字符+数值; | 进行了格式化输入控制; 1、能够正常输入 2、不允许输入 3、给予提示信息 4、不允许输入或提交时给予提示信息。 | |
输入整型数据,测试是否对数值型数据进行了格式化输入 | 1、输入正整数、0、负整数 2、输入小数 3、输入超大值(超出该字段定义范围) 4、输入非数值(如数值+字符;字符+数值;纯字符)
| 进行了格式化输入控制: 1、能够正常输入 2、不允许输入 3、给予提示信息 4、不允许输入或提交给予提示信息。 | |
输入浮点型数据,测试是否对数值型数据进行了格式化输入 | 1、输入正数、0、负数 2、输入超大值(超出该字段定义范围) 3、输入超出精度范围值(超出该字段定义精度) 4、输入非数值(如数值+字符;字符+数值;纯字符) | 进行了格式化输入控制: 4、不允许输入或提交给予提示信息。 | |
对于有图片上传功能的编辑框,若不上传图片,查看编辑页面时是否显示有默认的图片 | 最好设置一直默认的图片,当用户没有上传图片时,使用默认图片显示 | ||
提交数据时,连续多次点击,查看系统会不会连续增加几条相同的数据或报错。 | 避免用户多次点击情况 | ||
点击新增按钮,输入信息后,点重置 | 能够清空已输入的数据,但不清空新增时默认生成的数据 |
【15】邮箱格式
测试步骤 | 测试数据 | 预期结果 |
为必填项时,不输入任何字符或输入空格 | 不输入任何信息 | 校验不通过,给予提示信息 |
输入中文空格 | ||
输入英文空格 | ||
输入中文或英文 | ||
字符串中没有@和点 | ceshi163com 全部转成大写测试一遍 | |
字符串中有@和没有点 | ceshi@163com | |
字符串中没有@和有点 | ceshi163.com | |
字符串中有特殊字符 | $huiyanni@163.com | |
第一个字符串为@ 或点 | .test或@test | |
字符串的最后一位是@或点 | test@163. | |
@和点之间没有字符串 | liao@.com | |
字符串中多于两个@符号 | 123@qq@qq.com | |
字符串长度小于6,大于30个字符 | werw, wew...@163.com | |
字符串中有汉字 | 邮箱格式@qq.com | |
输入@前面有分隔符的情况 | a.b@163.com校验可通过 | |
输入@后面有多个分隔符(小数点) | ab@163.c.d校验可通过 |
【16】身份证号码
测试步骤 | 测试数据 | 预期结果 |
测试对身份证号码的溢出是否进行了控制 | 1、输入少于15位的身份证号码:如:36031219880708 2、输入大于15位的身份证号码:如:3603121988101058 3、输入少于18位的身份证号码:如:36031219897521456 4、输入大于18位的身份证号码:如:3603121987052874123
| 给予提示信息,不允许提交 |
测试是否对数字型数据是否进行了格式化输入 | 1、输入特殊字符串NULL、null、 空格的转义字符;<scrīpt></scrīpt>;<br>;<tr>;<td>;< /tr>等2、输入汉字 3、输入字母 | 进行了格式化输入,不可以输入非数字之外的数据 |
为必填项时,不输入任何字符或输入空格 | 1、不输入任何信息 2、输入中文空格 3、输入英文空格 | 必填,给予提示 |
测试是否对身份证号码进行了格式化输入(不存在的身份证号码) | 输入错误或不存在的身份证号码 332502201601012745 | 提示身份证号码不正确 |
输入的身份证号码是15位的真实号码(最后一位为字母或数字,字母是否一定是X,另外要求字母是否可以大小写都通过) | 提交成功 | |
输入的身份证号码是18位的真实号码(最后一位为字母或数字) | 提交成功 |
【17】电话号码
测试步骤 | 测试数据 | 预期结果 | 备注 |
测试是否对数字型数据是否进行了格式化输入 | 1、输入汉字或字母 2、输入特殊字符串NULL、null、 空格的转义字符;<scrīpt></scrīpt>;<br>;<tr>;<td>;< /tr>;</td>;</html>;</body>;</table>等“-”除外 | 校验不通过,给予提示信息 | 区号:3-5位,如北京:010、杭州0571、贵州福泉08641
常见的固定电话号码格式为区号加“-”加7位或8位电话号码 |
为必填项时,不输入任费时何字符或输入空格 | 不输入任何信息 输入中文空格 输入英文空格 | 校验不通过,给予提示信息 | 格式:+ 86 571 8667 4288、0571-87654321、571-87654321、(0571)87654321、87654321、95105888
服务热线电话:95504、4008-114-124、400-665-9999等 |
【18】手机号码
测试步骤 | 测试数据 | 预期结果 | 备注 |
为必填项时,不输入任何字符或输入空格 | 输入英文空格 输入中文空格 不输入任何信息 | 校验不通过,给予提示信息 | 格式:校验数字开头或者目前是数字1开头的 测试数据:135-1234-1234、+8612345678900、+86 12345678900、(86)12345678900、23415678900 |
输入长度超过11位字符 | 例如:158581654789 | 校验不通过,给予提示信息 | |
输入长度小于11位字符 | 例如:158、15825、15858125、1585814569等 | 校验不通过,给予提示信息 | 特殊的 147、157、183、188、189等 |
测试是否对数字型数据是否进行了格式化输入 | 输入特殊字符串NULL、null、 空格的转义字符; 输入汉字或字母 | 进行了格式化输入,不可以输入非数字之外的数据 | |
测试是否对手机号码进行了格式化输入 | 除这些号码以外的:联通:130-132,155,156,186 移动:134-139,150-152,157-159,188 电信:133,153,189,180 | 进行了格式化输入 | 最好校验一下长度是11位数字,1开头即可 |
【19】邮政编码
测试步骤 | 测试数据 | 预期结果 |
输入英文空格/不输入任何信息/输入中文空格 | 给予提示信息,不允许提交 | |
输入特殊字符 | 给予提示信息,不允许提交 | |
输入中、英文内容/中英文结合数字 | 给予提示信息,不允许提交 | |
输入空格+数字,空格出现在开头、中间、结尾均需测试 | 前后去掉空格,中间含空格给予提示 | |
输入小于6个数字 | 给予提示信息,不允许提交 | |
输入6位数字 | 通过 | |
输入大于6位位数字 | 给予提示信息,不允许提交 |
【20】注册
测试步骤 | 测试数据 | 预期结果 |
不输入任何信息,直接点注册,验证是否必填 | 提示不能为空 | |
输入用户名、密码和确认密码,点击提交表单 | 用户名:test 密码:123456 确认密码:123456 | 注册成功 |
输入用户名、密码和确认密码,用户名以字符开头 | 用户名:-test001 密码:123456 确认密码:123456 | 提示"用户名只能以数字或字母开头或结尾,请重新输入" |
输入用户名和密码,用户名含有非法字符 | 用户名:a0571&00 密码:123 | 提示用户名含有非法字符 |
输入用户名和密码,密码含有非法字符 | 用户名:a0571 密码:123*123 | 提示密码含有非法字符 |
输入用户名、密码和确认密码,两次密码输入不一致 | 用户名:a0571 密码:123 确认密码:456 | 提示两次密码输入不一致,请重新输入 |
输入用户名、密码和确认密码、邮箱,邮箱格式的不正确 | 用户名:a0571 密码:123 确认密码:123 邮箱:4412854qq.com | 提示邮箱地址不正确不正确 |
输入用户名和密码,输入的用户名已经被注册 | 用户名:test 密码:123456 | 提示用户名已经被注册,请重新输入 |
输入用户名和密码,用户名和密码长度都是最大值 | 用户名:testtest0123456789 密码:360312196808061028 | 注册成功 |
输入用户名和密码,用户名长度超过最大值 | 用户名:testtest01234567890 密码:123456 | 注册失败,提示用户名的长度超过最大有效值 |
输入用户名和密码,密码长度超过最大值 | 用户名:test 密码:3603121968080610280 | 注册失败,提示密码的长度超过最大有效值 |
输入用户名和密码,用户名和密码长度都是最小值 | 用户名:test01 密码:123456 | 注册成功 |
输入用户名和密码,用户名长度小于最小值 | 用户名:test 密码:123456 | 注册失败,提示用户名或密码的长度低于最小有效值 |
输入用户名和密码,密码长度小于最小值 | 用户名:test01 密码:123 | |
输入用户名和密码,验证是否支持快捷键tab键 | 鼠标光标依次跳转到用户名、密码、确认密码等 | |
输入已经存在的用户名,验证对已经存在用户的用户名大小写是否做出正确响应 | 用户名:Test001 | 注册成功(用户名区分大小写) |
【21】登录
测试步骤 | 测试数据 | 预期结果 |
输入正确的用户名和密码登陆 | 用户名:ceshi 密码:123465 | 登陆成功 |
不输入任何信息,点击登录 | 无法登录,提示请用户名和密码不能为空 | |
输入存在的用户名,密码为空,点击登录 | 用户名:test | 无法登录,提示请密码不能为空 |
输入存在的密码,用户名为空,点击登录 | 密码:123456 | 无法登录,提示用户名不能为空 |
输入存在的用户名、不存在的密码,点击登录 | 用户名:test 密码:456 | 无法登录,提示请输入正确的密码 |
输入不存在的用户名、存在的密码,点击登录 | 用户名:test1 密码:123456 | 无法登录,提示该用户名不存在 |
输入不存在的用户名、不存在的密码,点击登录 | 用户名:test1 密码:123 | 无法登录,提示请输入正确的用户名或密码 |
连续输入三次用户名和密码不正确,点击登录 | 无法登录,提示:您没有使用该系统的权限,请与管理员联系! | |
输入正确的用户名和密码,但用户名未区分小写 | 用户名:A001 密码:123 | 区分大小写,不能正常登录,提示请输入正确的用户名或密码 |
输入正确的用户名和密码,但密码未区分小写 | 用户名:a001 密码:A123 | 区分大小写,不能正常登录,提示请输入正确的的密码 |
在合法的用户名或密码前插入空格 | 用户名:空格a001 密码:空格123 | 提示请输入正确的用户名或密码 |
在合法的用户名或密码中间插入空格 | 用户名:aa空格a001 密码:11空格123 | 提示请输入正确的用户名或密码 |
在合法的用户名或密码后插入空格 | 用户名:aa001空格 密码:111空格 | 提示请输入正确的用户名或密码 |
输入已经禁止的用户名 | 用户名:test 密码:123456 | 您输入的用户名已经被禁止 |
输入已经删除的用户名 | 用户名:test 密码:123456 | 您输入的用户名不存在 |
验证是否支持快捷键tab键 | 鼠标光标依次跳转到用户名、密码、登录按钮上 | |
用全三角输入法输入用户名或密码 | 用户名:abc 密码:123 | 请输入正确的用户名或密码 |
用户重置或取消:输入用户名或密码,点击重置或取消 | 用户名:test 密码:123456 | 清空输入框 |
登录时,当页面刷新或重新输入数据时,验证码是否更新 | 点击获取印证码后,刷新页面 | 印证码刷新 |
【22】IP地址
测试步骤 | 测试数据 | 预期结果 |
测试必填:输入中英文空格或者不输入任何信息 | 给予提示信息,不允许提交 | |
IP地址 前中后含空格 | 中间不允许包含空格 | |
要求是4位,输入小于4位或者大于4位格式 | 137.125.11 137.112.254.12.12 | 给予提示信息,不允许提交 |
4位中有任意一位大于255(测试每一位) | 192.124.256.21 | 给予提示信息,不允许提交 |
第一个数字或者最后一个数字为0 | 0.1.2.3 255.255.255.0 23.54.156.0等 | 给予提示信息,不允许提交 |
其中某一位超过4个数字 | 145.1234.12.111 | 给予提示信息,不允许提交 |
特殊的IP | 255.255.255.255 0.0.0.0 1.1.1.1 | |
正确的ip地址中混入非法字符/中文/英文 | 给予提示信息,不允许提交 |
React测试用例代码篇:
项目用到的技术框架:
该项目采用 react 技术栈,用到的主要框架包括:react、redux、react-redux、redux-actions、reselect、redux-saga、seamless-immutable、antd。
应用场景介绍:

这个应用场景从 UI 层来讲主要由两个部分组成:
- 工具栏,包含刷新按钮、关键字搜索框
- 表格展示,采用分页的形式浏览
别急,为了保证文章的阅读体验和长度适中,能讲清楚问题的简洁场景就是好场景不是吗?慢慢往下看。
设计模式与结构分析
在这个场景设计开发中,我们严格遵守 redux 单向数据流 与 react-redux 的最佳实践,并采用 redux-saga 来处理业务流,reselect 来处理状态缓存,通过 fetch 来调用后台接口,与真实的项目没有差异。
分层设计与代码组织如下所示:

中间 store 中的内容都是 redux 相关的,看名称应该都能知道意思了。
代码下载。
单元测试部分介绍:
先讲一下用到了哪些测试框架和工具,主要内容包括:
- jest ,测试框架
- enzyme ,专测 react ui 层
- sinon ,具有独立的 fakes、spies、stubs、mocks 功能库
- nock ,模拟 HTTP Server
接下来,我们就开始编写具体的测试用例代码了,下面会针对每个层面给出代码片段和解析。那么我们先从 actions 开始吧。
actions
业务里面我使用了 redux-actions 来产生 action,这里用工具栏做示例,先看一段业务代码:
import { createAction } from 'redux-actions';
import * as type from '../types/bizToolbar';
export const updateKeywords = createAction(type.BIZ_TOOLBAR_KEYWORDS_UPDATE);
// ...
对于 actions 测试,我们主要是验证产生的 action 对象是否正确:
import * as type from '@/store/types/bizToolbar';
import * as actions from '@/store/actions/bizToolbar';
/* 测试 bizToolbar 相关 actions */
describe('bizToolbar actions', () => {
/* 测试更新搜索关键字 */
test('should create an action for update keywords', () => {
// 构建目标 action
const keywords = 'some keywords';
const expectedAction = {
type: type.BIZ_TOOLBAR_KEYWORDS_UPDATE,
payload: keywords
};
// 断言 redux-actions 产生的 action 是否正确
expect(actions.updateKeywords(keywords)).toEqual(expectedAction);
});
// ...
});
这个测试用例的逻辑很简单,首先构建一个我们期望的结果,然后调用业务代码,最后验证业务代码的运行结果与期望是否一致。这就是写测试用例的基本套路。
我们在写测试用例时尽量保持用例的单一职责,不要覆盖太多不同的业务范围。测试用例数量可以有很多个,但每个都不应该很复杂。
reducers
接着是 reducers,依然采用 redux-actions 的 handleActions 来编写 reducer,这里用表格的来做示例:
import { handleActions } from 'redux-actions';
import Immutable from 'seamless-immutable';
import * as type from '../types/bizTable';
/* 默认状态 */
export const defaultState = Immutable({
loading: false,
pagination: {
current: 1,
pageSize: 15,
total: 0
},
data: []
});
export default handleActions(
{
// ...
/* 处理获得数据成功 */
[type.BIZ_TABLE_GET_RES_SUCCESS]: (state, {payload}) => {
return state.merge(
{
loading: false,
pagination: {total: payload.total},
data: payload.items
},
{deep: true}
);
},
// ...
},
defaultState
);
这里的状态对象使用了 seamless-immutable
对于 reducer,我们主要测试两个方面:
- 对于未知的 action.type ,是否能返回当前状态。
- 对于每个业务 type ,是否都返回了经过正确处理的状态。
import * as type from '@/store/types/bizTable';
import reducer, { defaultState } from '@/store/reducers/bizTable';
/* 测试 bizTable reducer */
describe('bizTable reducer', () => {
/* 测试未指定 state 参数情况下返回当前缺省 state */
test('should return the default state', () => {
expect(reducer(undefined, {type: 'UNKNOWN'})).toEqual(defaultState);
});
// ...
/* 测试处理正常数据结果 */
test('should handle successful data response', () => {
/* 模拟返回数据结果 */
const payload = {
items: [
{id: 1, code: '1'},
{id: 2, code: '2'}
],
total: 2
};
/* 期望返回的状态 */
const expectedState = defaultState
.setIn(['pagination', 'total'], payload.total)
.set('data', payload.items)
.set('loading', false);
expect(
reducer(defaultState, {
type: type.BIZ_TABLE_GET_RES_SUCCESS,
payload
})
).toEqual(expectedState);
});
// ...
});
这里的测试用例逻辑也很简单,依然是上面断言期望结果的套路。下面是 selectors 的部分。
selectors
selector 的作用是获取对应业务的状态,这里使用了 reselect 来做缓存,防止 state 未改变的情况下重新计算,先看一下表格的 selector 代码:
import { createSelector } from 'reselect';
import * as defaultSettings from '@/utils/defaultSettingsUtil';
// ...
const getBizTableState = (state) => state.bizTable;
export const getBizTable = createSelector(getBizTableState, (bizTable) => {
return bizTable.merge({
pagination: defaultSettings.pagination
}, {deep: true});
});
这里的分页器部分参数在项目中是统一设置,所以 reselect 很好的完成了这个工作:如果业务状态不变,直接返回上次的缓存。分页器默认设置如下:
export const pagination = {
size: 'small',
showTotal: (total, range) => `${range[0]}-${range[1]} / ${total}`,
pageSizeOptions: ['15', '25', '40', '60'],
showSizeChanger: true,
showQuickJumper: true
};
那么我们的测试也主要是两个方面:
- 对于业务 selector ,是否返回了正确的内容。
- 缓存功能是否正常。
import Immutable from 'seamless-immutable';
import { getBizTable } from '@/store/selectors';
import * as defaultSettingsUtil from '@/utils/defaultSettingsUtil';
/* 测试 bizTable selector */
describe('bizTable selector', () => {
let state;
beforeEach(() => {
state = createState();
/* 每个用例执行前重置缓存计算次数 */
getBizTable.resetRecomputations();
});
function createState() {
return Immutable({
bizTable: {
loading: false,
pagination: {
current: 1,
pageSize: 15,
total: 0
},
data: []
}
});
}
/* 测试返回正确的 bizTable state */
test('should return bizTable state', () => {
/* 业务状态 ok 的 */
expect(getBizTable(state)).toMatchObject(state.bizTable);
/* 分页默认参数设置 ok 的 */
expect(getBizTable(state)).toMatchObject({
pagination: defaultSettingsUtil.pagination
});
});
/* 测试 selector 缓存是否有效 */
test('check memoization', () => {
getBizTable(state);
/* 第一次计算,缓存计算次数为 1 */
expect(getBizTable.recomputations()).toBe(1);
getBizTable(state);
/* 业务状态不变的情况下,缓存计算次数应该还是 1 */
expect(getBizTable.recomputations()).toBe(1);
const newState = state.setIn(['bizTable', 'loading'], true);
getBizTable(newState);
/* 业务状态改变了,缓存计算次数应该是 2 了 */
expect(getBizTable.recomputations()).toBe(2);
});
});
测试用例依然很简单有木有?保持这个节奏就对了。下面来讲下稍微有点复杂的地方,sagas 部分。
sagas
这里我用了 redux-saga 处理业务流,这里具体也就是异步调用 api 请求数据,处理成功结果和错误结果等。
可能有的童鞋觉得搞这么复杂干嘛,异步请求用个 redux-thunk 不就完事了吗?别急,耐心看完你就明白了。
这里有必要大概介绍下 redux-saga 的工作方式。saga 是一种 es6 的生成器函数 - Generator ,我们利用他来产生各种声明式的 effects ,由 redux-saga 引擎来消化处理,推动业务进行。
这里我们来看看获取表格数据的业务代码:
import { all, takeLatest, put, select, call } from 'redux-saga/effects';
import * as type from '../types/bizTable';
import * as actions from '../actions/bizTable';
import { getBizToolbar, getBizTable } from '../selectors';
import * as api from '@/services/bizApi';
// ...
export function* onGetBizTableData() {
/* 先获取 api 调用需要的参数:关键字、分页信息等 */
const {keywords} = yield select(getBizToolbar);
const {pagination} = yield select(getBizTable);
const payload = {
keywords,
paging: {
skip: (pagination.current - 1) * pagination.pageSize, max: pagination.pageSize
}
};
try {
/* 调用 api */
const result = yield call(api.getBizTableData, payload);
/* 正常返回 */
yield put(actions.putBizTableDataSuccessResult(result));
} catch (err) {
/* 错误返回 */
yield put(actions.putBizTableDataFailResult());
}
}
不熟悉 redux-saga 的童鞋也不要太在意代码的具体写法,看注释应该能了解这个业务的具体步骤:
从对应的 state 里取到调用 api 时需要的参数部分(搜索关键字、分页),这里调用了刚才的 selector。
组合好参数并调用对应的 api 层。
如果正常返回结果,则发送成功 action 通知 reducer 更新状态。
如果错误返回,则发送错误 action 通知 reducer。
那么具体的测试用例应该怎么写呢?我们都知道这种业务代码涉及到了 api 或其他层的调用,如果要写单元测试必须做一些 mock 之类来防止真正调用 api 层,下面我们来看一下 怎么针对这个 saga 来写测试用例:
import { put, select } from 'redux-saga/effects';
// ...
/* 测试获取数据 */
test('request data, check success and fail', () => {
/* 当前的业务状态 */
const state = {
bizToolbar: {
keywords: 'some keywords'
},
bizTable: {
pagination: {
current: 1,
pageSize: 15
}
}
};
const gen = cloneableGenerator(saga.onGetBizTableData)();
/* 1. 是否调用了正确的 selector 来获得请求时要发送的参数 */
expect(gen.next().value).toEqual(select(getBizToolbar));
expect(gen.next(state.bizToolbar).value).toEqual(select(getBizTable));
/* 2. 是否调用了 api 层 */
const callEffect = gen.next(state.bizTable).value;
expect(callEffect['CALL'].fn).toBe(api.getBizTableData);
/* 调用 api 层参数是否传递正确 */
expect(callEffect['CALL'].args[0]).toEqual({
keywords: 'some keywords',
paging: {skip: 0, max: 15}
});
/* 3. 模拟正确返回分支 */
const successBranch = gen.clone();
const successRes = {
items: [
{id: 1, code: '1'},
{id: 2, code: '2'}
],
total: 2
};
expect(successBranch.next(successRes).value).toEqual(
put(actions.putBizTableDataSuccessResult(successRes)));
expect(successBranch.next().done).toBe(true);
/* 4. 模拟错误返回分支 */
const failBranch = gen.clone();
expect(failBranch.throw(new Error('模拟产生异常')).value).toEqual(
put(actions.putBizTableDataFailResult()));
expect(failBranch.next().done).toBe(true);
});
这个测试用例相比前面的复杂了一些,我们先来说下测试 saga 的原理。前面说过 saga 实际上是返回各种声明式的 effects ,然后由引擎来真正执行。所以我们测试的目的就是要看 effects 的产生是否符合预期。那么effect 到底是个神马东西呢?其实就是字面量对象!
我们可以用在业务代码同样的方式来产生这些字面量对象,对于字面量对象的断言就非常简单了,并且没有直接调用 api 层,就用不着做 mock 咯!这个测试用例的步骤就是利用生成器函数一步步的产生下一个 effect ,然后断言比较。
从上面的注释 3、4 可以看到,redux-saga 还提供了一些辅助函数来方便的处理分支断点。
这也是我选择 redux-saga 的原因:强大并且利于测试。
api 和 fetch 工具库
接下来就是api 层相关的了。前面讲过调用后台请求是用的 fetch ,我封装了两个方法来简化调用和结果处理:getJSON() 、postJSON() ,分别对应 GET 、POST 请求。先来看看 api 层代码:
import { fetcher } from '@/utils/fetcher';
export function getBizTableData(payload) {
return fetcher.postJSON('/api/biz/get-table', payload);
}
业务代码很简单,那么测试用例也很简单:
import sinon from 'sinon';
import { fetcher } from '@/utils/fetcher';
import * as api from '@/services/bizApi';
/* 测试 bizApi */
describe('bizApi', () => {
let fetcherStub;
beforeAll(() => {
fetcherStub = sinon.stub(fetcher);
});
// ...
/* getBizTableData api 应该调用正确的 method 和传递正确的参数 */
test('getBizTableData api should call postJSON with right params of fetcher', () => {
/* 模拟参数 */
const payload = {a: 1, b: 2};
api.getBizTableData(payload);
/* 检查是否调用了工具库 */
expect(fetcherStub.postJSON.callCount).toBe(1);
/* 检查调用参数是否正确 */
expect(fetcherStub.postJSON.lastCall.calledWith('/api/biz/get-table', payload)).toBe(true);
});
});
由于 api 层直接调用了工具库,所以这里用 sinon.stub() 来替换工具库达到测试目的。
接着就是测试自己封装的 fetch 工具库了,这里 fetch 我是用的 isomorphic-fetch ,所以选择了 nock 来模拟 Server 进行测试,主要是测试正常访问返回结果和模拟服务器异常等,示例片段如下:
import nock from 'nock';
import { fetcher, FetchError } from '@/utils/fetcher';
/* 测试 fetcher */
describe('fetcher', () => {
afterEach(() => {
nock.cleanAll();
});
afterAll(() => {
nock.restore();
});
/* 测试 getJSON 获得正常数据 */
test('should get success result', () => {
nock('http://some')
.get('/test')
.reply(200, {success: true, result: 'hello, world'});
return expect(fetcher.getJSON('http://some/test')).resolves.toMatch(/^hello.+$/);
});
// ...
/* 测试 getJSON 捕获 server 大于 400 的异常状态 */
test('should catch server status: 400+', (done) => {
const status = 500;
nock('http://some')
.get('/test')
.reply(status);
fetcher.getJSON('http://some/test').catch((error) => {
expect(error).toEqual(expect.any(FetchError));
expect(error).toHaveProperty('detail');
expect(error.detail.status).toBe(status);
done();
});
});
/* 测试 getJSON 传递正确的 headers 和 query strings */
test('check headers and query string of getJSON()', () => {
nock('http://some', {
reqheaders: {
'Accept': 'application/json',
'authorization': 'Basic Auth'
}
})
.get('/test')
.query({a: '123', b: 456})
.reply(200, {success: true, result: true});
const headers = new Headers();
headers.append('authorization', 'Basic Auth');
return expect(fetcher.getJSON(
'http://some/test', {a: '123', b: 456}, headers)).resolves.toBe(true);
});
// ...
});
基本也没什么复杂的,主要注意 fetch 是 promise 返回,jest 的各种异步测试方案都能很好满足。
剩下的部分就是跟 UI 相关的了。
容器组件
容器组件的主要目的是传递 state 和 actions,看下工具栏的容器组件代码:
import { connect } from 'react-redux';
import { getBizToolbar } from '@/store/selectors';
import * as actions from '@/store/actions/bizToolbar';
import BizToolbar from '@/components/BizToolbar';
const mapStateToProps = (state) => ({
...getBizToolbar(state)
});
const mapDispatchToProps = {
reload: actions.reload,
updateKeywords: actions.updateKeywords
};
export default connect(mapStateToProps, mapDispatchToProps)(BizToolbar);
那么测试用例的目的也是检查这些,这里使用了 redux-mock-store 来模拟 redux 的 store :
import React from 'react';
import { shallow } from 'enzyme';
import configureStore from 'redux-mock-store';
import BizToolbar from '@/containers/BizToolbar';
/* 测试容器组件 BizToolbar */
describe('BizToolbar container', () => {
const initialState = {
bizToolbar: {
keywords: 'some keywords'
}
};
const mockStore = configureStore();
let store;
let container;
beforeEach(() => {
store = mockStore(initialState);
container = shallow(<BizToolbar store={store}/>);
});
/* 测试 state 到 props 的映射是否正确 */
test('should pass state to props', () => {
const props = container.props();
expect(props).toHaveProperty('keywords', initialState.bizToolbar.keywords);
});
/* 测试 actions 到 props 的映射是否正确 */
test('should pass actions to props', () => {
const props = container.props();
expect(props).toHaveProperty('reload', expect.any(Function));
expect(props).toHaveProperty('updateKeywords', expect.any(Function));
});
});
很简单有木有,所以也没啥可说的了。
UI 组件
这里以表格组件作为示例,我们将直接来看测试用例是怎么写。一般来说 UI 组件我们主要测试以下几个方面:
- 是否渲染了正确的 DOM 结构
- 样式是否正确
- 业务逻辑触发是否正确
import React from 'react';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { Table } from 'antd';
import * as defaultSettingsUtil from '@/utils/defaultSettingsUtil';
import BizTable from '@/components/BizTable';
/* 测试 UI 组件 BizTable */
describe('BizTable component', () => {
const defaultProps = {
loading: false,
pagination: Object.assign({}, {
current: 1,
pageSize: 15,
total: 2
}, defaultSettingsUtil.pagination),
data: [{id: 1}, {id: 2}],
getData: sinon.fake(),
updateParams: sinon.fake()
};
let defaultWrapper;
beforeEach(() => {
defaultWrapper = mount(<BizTable {...defaultProps}/>);
});
// ...
/* 测试是否渲染了正确的功能子组件 */
test('should render table and pagination', () => {
/* 是否渲染了 Table 组件 */
expect(defaultWrapper.find(Table).exists()).toBe(true);
/* 是否渲染了 分页器 组件,样式是否正确(mini) */
expect(defaultWrapper.find('.ant-table-pagination.mini').exists()).toBe(true);
});
/* 测试首次加载时数据列表为空是否发起加载数据请求 */
test('when componentDidMount and data is empty, should getData', () => {
sinon.spy(BizTable.prototype, 'componentDidMount');
const props = Object.assign({}, defaultProps, {
pagination: Object.assign({}, {
current: 1,
pageSize: 15,
total: 0
}, defaultSettingsUtil.pagination),
data: []
});
const wrapper = mount(<BizTable {...props}/>);
expect(BizTable.prototype.componentDidMount.calledOnce).toBe(true);
expect(props.getData.calledOnce).toBe(true);
BizTable.prototype.componentDidMount.restore();
});
/* 测试 table 翻页后是否正确触发 updateParams */
test('when change pagination of table, should updateParams', () => {
const table = defaultWrapper.find(Table);
table.props().onChange({current: 2, pageSize: 25});
expect(defaultProps.updateParams.lastCall.args[0])
.toEqual({paging: {current: 2, pageSize: 25}});
});
});
得益于设计分层的合理性,我们很容易利用构造 props 来达到测试目的,结合 enzyme 和 sinon ,测试用例依然保持简单的节奏。
- 上一篇:php、js实现手机号中间星号*代替显示
- 下一篇:php判断是否关注微信公众号