【山竹记账后端】3.后端单元测试
大纲链接 §
[toc]
避免联调时才发现bug,后端需要对每个接口进行单元测试;前后端开发的顺序:
- 先写一部分测试
- 实现测试的功能后端代码
- 补充测试
回想之前如何测试 API?
- 使用
curl命令 构造接口请求- 发送
GET请求 curl http://localhost:3000/api/v1/tags?page=1- 看响应 添加参数
-v - 发送
POST请求 curl -X POST http://localhost:3000/api/v1/tags- 添加请求头
-H 'Content-Type: application/json' - 添加消息体
-d '{"amount": 99}' - 发送其他请求
curl -X PATCH
- 发送
- 使用
postman之类的接口工具
使用
curl的弊端
- 难写
- 难批量
- 难重复操作
使用
postman的弊端
- 在
Collection中,单独写自动化测试断言和脚本、录入接口再执行; - 与源代码分离,逻辑分散不内聚
- 团队协作体验差,免费版有团队人数和分享额度限制,高级功能收费昂贵,自定义定制难
- 复杂业务流控死板、复杂的业务逻辑,不如直接编写代码灵活
使用纯代码框架可以解决以上困难,请问哪里找好用的测试框架?
- Ruby ToolBox 可从各个方面评判库的活跃度
- 选 minitest 还是 rspec 请从各个方面对比优缺点
评判一个库,需要关注哪些方面
- 最近三年的活跃程度
- 是否有长期的更新
- 更新年份的分布(
RSpec随Rails一起成长,早于Github成立) - 下载数、
forks数、issue关闭数关闭率、PR接受率 等 star数(不一定,monorepo项目库分流;是否早于Github成立,Github是Rails写的)
RSpec比Rails自带的minitest更爽一点,更像是DSL,符合 Spec 风格(describe/it)BDD风格
0.单元测试要测什么 ⇧
- 目前大都只测试
Controllers- 例如 登录
jwt等
- 例如 登录
- 因为目前项目的
Models和Views都很简单json字段格式类型是否符合- 数据数量、字段等
不测哪些
- 不测
Rails自带的功能,因为Rails测过了- 比如
validate功能本身
- 比如
- 不测 第三方功能,因为他们应该自己测,直接
mock掉- 比如发邮件功能,邮件是否送达
1. 安装 RSpec ⇧
打开
Gemfile,将gem 'rspec-rails', '~> 8.0.0'复制到group :development, :test do
|
|
- 将其添加到
:development组中并不是必须的,- 但如果没有这样做,那么生成器和回收任务就必须以
RAILS_ENV=test作为前缀来标识。
- 但如果没有这样做,那么生成器和回收任务就必须以
- 然后运行
bundle install --verbose或bundle --verbose- 可以切换国内源,加速安装
初始化
RSpec,生成项目依赖引入入口文件
- 运行
bin/rails generate rspec:install,自动创建几个帮助文件.rspec依赖spec_helper帮助方法spec/spec/rails_helper.rbspec/spec_helper.rb
2. 使用测试数据库 ⇧
从初始测试用例开始:
user演示
- 由于之前已经创建过了
bin/rails generate model user - 可以直接初始话对应的测试文件,运行
bin/rails generate rspec:model user
创建了
spec/models/user_spec.rb
|
|
require 'rails_helper'引入spec_helper帮助方法RSpec.describe UserBDD风格描述,接受两个参数:type: :model指定类型- 代码块
do ... end
运行
bundle exec rspec报错
|
|
- 提示未连接数据库,在外部系统命令中启动
docker start db-for-mangosteen
运行
bundle exec rspec还是报错
- 是由于目前只配置了开发环境数据库,测试环境数据库未配置
配置测试数据库 ⇧
在
config/database.yml中配置
|
|
创建测试数据库 ⇧
- 运行
docker exec -it db-for-mangosteen bash打开终端创建,步骤复杂exit 使用
Rails创建,运行命令:RAILS_ENV=test bin/rails db:create1 2RAILS_ENV=test bin/rails db:create # Created database 'mangosteen_1_test'
迁移数据库创建表 ⇧
运行命令
RAILS_ENV=test bin/rails db:migrate1 2 3 4 5 6 7 8 9 10 11 12 13 14 15RAILS_ENV=test bin/rails db:migrate == 20260906082930 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0134s == 20260906082930 CreateUsers: migrated (0.0135s) ============================= == 20260909153656 CreateValidationCodes: migrating ============================ -- create_table(:validation_codes) -> 0.0114s == 20260909153656 CreateValidationCodes: migrated (0.0114s) =================== == 20260910063034 CreateItems: migrating ====================================== -- create_table(:items) -> 0.0114s == 20260910063034 CreateItems: migrated (0.0114s) =============================
此时再次运行测试启动命令
bundle exec rspec
修改测试用例 ⇧
spec/models/user_spec.rb
|
|
运行测试启动命令
bundle exec rspec,报错
|
|
看报错提示使用
eq修改对象属性user.emailto be比较两个对象 是否完全相同,不适合1 2 3 4 5 6 7 8 9 10 11 12require 'rails_helper' RSpec.describe User, type: :model do it '有 email' do user = User.create email: 'frank@1.com' p "user----------------" p user p "user----------------" expect(user.email).to eq 'frank@1.com' end end
提示成功
回顾用了哪些命令 ⇧
终端运行
history查看历史命令1 2 3 4 5 6 7 8 9 10 11 12# GemFile添加依赖后手动安装 bundle --verbose # 创建 约定的 测试帮助依赖 bin/rails generate rspec:install # 创建 约定的 测试文件(已配置 config/database.yml test) bin/rails generate rspec:model user # 创建测试数据库 RAILS_ENV=test bin/rails db:create # 迁移数据库创建表 RAILS_ENV=test bin/rails db:migrate # 运行测试 bundle exec rspec
参考
3. 如何测试请求(Controller) ⇧
创建测试用例,并验证 ⇧
由于使用的时
rails的api模式,目前只是用RSpec的request test功能(暂不使用其controller test)
运行命令
bin/rails generate rspec:request items创建 约定的 测试文件1 2bin/rails generate rspec:request items # create spec/requests/items_spec.rb
spec/requests/items_spec.rb
|
|
RSpec.describe "Items", type: :request接受两个参数:type: :request指定类型- 代码块
do ... end
查看路由
config/routes.rb
|
|
- 判断
items的各个请求方法都有 - 直接运行测试命令
bundle exec rspec
测试未通过,报错
|
|
- 提示找不到
items_index_path,修改为真实url ‘/api/v1/items’
spec/requests/items_spec.rb
|
|
- 运行成功
验证是否改了
app/controllers/api/v1/items_controller.rb后测试失败
|
|
- 修改返回参数
status: 201 再运行测试
bundle exec rspec1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16bundle exec rspec no .F Failures: 1) Items GET /items works! (now write some real specs) Failure/Error: expect(response).to have_http_status(200) expected the response to have status code 200 but it was 201 # ./spec/requests/items_spec.rb:7:in `block (3 levels) in <main>' Finished in 0.07689 seconds (files took 0.8816 seconds to load) 2 examples, 1 failure Failed examples: rspec ./spec/requests/items_spec.rb:5 # Items GET /items works! (now write some real specs)确保准确地是在测试该
controller
参考
- Request specs
- Introduce RSpec Request Spec
- Testing in Rails: RSpec Tips and Tricks
- RSpec — Controller or Request Specs?
构造测试数据 ⇧
spec/requests/items_spec.rb
|
|
- 构造11项数据
11.times do ... end- 构造一个对象
Item.new amount: 100
- 构造一个对象
- 期待
Item.count的数量为11 - 发请求,期待
response.status为200 - 解析json,期待
resources['resources'].size为10 运行
bundle exec rspec,看报错1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20bundle exec rspec no .F Failures: 1) Items GET /items works! (now write some real specs) Failure/Error: expect(Item.count).to eq(11) expected: 11 got: 0 (compared using ==) # ./spec/requests/items_spec.rb:9:in `block (3 levels) in <main>' Finished in 0.02781 seconds (files took 0.51029 seconds to load) 2 examples, 1 failure Failed examples: rspec ./spec/requests/items_spec.rb:5 # Items GET /items works! (now write some real specs)
添加打印信息,再看报错
spec/requests/items_spec.rb
|
|
运行
bundle exec rspec,看报错1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23bundle exec rspec no ."11.times------------------" Item(id: integer, user_id: integer, amount: integer, note: text, tags_id: integer, happened_at: datetime, created_at: datetime, updated_at: datetime) "------------------11.times" F Failures: 1) Items GET /items works! (now write some real specs) Failure/Error: expect(Item.count).to eq(11) expected: 11 got: 0 (compared using ==) # ./spec/requests/items_spec.rb:12:in `block (3 levels) in <main>' Finished in 0.0297 seconds (files took 0.85805 seconds to load) 2 examples, 1 failure Failed examples: rspec ./spec/requests/items_spec.rb:5 # Items GET /items works! (now write some real specs)未将构造的数据保存到数据库中
Item.save可用Item.create替代1 2 3 4 5bundle exec rspec no .. Finished in 0.10114 seconds (files took 0.77976 seconds to load) 2 examples, 0 failures一句逻辑,一句断言
测试第二页
spec/requests/items_spec.rb
|
|
测试如何确定创建Item成功 ⇧
创建
Item成功的确凿证据时数据库变了spec/requests/items_spec.rb
|
|
- 每个测试用力之间不应该有任何干扰,这样移动任何一个用力顺序的时候,就完全不会受影响
rails rspec只要运行完一个测试用例,会自动清空数据(保留表),下次再重新创建运行
bundle exec rspec1 2 3 4... Finished in 0.10483 seconds (files took 0.82176 seconds to load) 3 examples, 0 failures
尝试运行失败的用例
spec/requests/items_spec.rb
|
|
运行
bundle exec rspec1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19..F Failures: 1) Items create can create a item Failure/Error: expect(Item.count).to eq(2) expected: 2 got: 1 (compared using ==) # ./spec/requests/items_spec.rb:26:in `block (3 levels) in <main>' Finished in 0.10106 seconds (files took 0.44388 seconds to load) 3 examples, 1 failure Failed examples: rspec ./spec/requests/items_spec.rb:23 # Items create can create a item
测试断言改变数据 ⇧
spec/requests/items_spec.rb
|
|
- 运行
bundle exec rspec - 其实
change { Item.count }.from(0).to(1)这种写法并不好- 因为有时候并不能确定数据的总数,不能给出确定数量的断言
改为
change { Item.count }.by(+1)by(1)也可,by(+1)意思更明确1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18require 'rails_helper' RSpec.describe "Items", type: :request do describe "index by page" do it "验证了共11条数据,每页10条" do #... end end describe "create" do ite "can create a item" do expect { post '/api/v1/items', params: { amount: 99 } }.to change { Item.count }.by(+1) # change { Item.count }.by(-1) end end end
参考
测试创建返回字段 ⇧
spec/requests/items_spec.rb
|
|
- 成功运行一个测视用例后,一定要尝试修改用例是否会报错,会报错说明测试代码正确处理
- 类似
.to eq(99)的最后一个括号可以不写,看上去更像英文 - 但中间的括号必须写,因为需要链式调用
spec/requests/items_spec.rb
|
|
- 这个用例做了:
- 传参发请求
- 验证数据库数据
- 验证状态码
- 解析
json - 解析
json中字段,验证
运行
bundle exec rspec查看报错1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20bundle exec rspec no ..F Failures: 1) Items create can create a item Failure/Error: expect(json['resource']['amount']).to eq 99 expected: 99 got: 1 (compared using ==) # ./spec/requests/items_spec.rb:29:in `block (3 levels) in <main>' Finished in 0.08767 seconds (files took 0.42069 seconds to load) 3 examples, 1 failure Failed examples: rspec ./spec/requests/items_spec.rb:23 # Items create can create a item由于之前的
app/controllers/api/v1/items_controller.rb创建逻辑中写死了数量
app/controllers/api/v1/items_controller.rb
|
|
如果有多个参数,需要每个都写出来吗? 例如:
amount: params[:amount], note: params[:note], ...
- 后面处理,
ruby动态语言已经有成熟的处理方法;java就必须都一一写出来 运行
bundle exec rspec查看报错1 2 3 4 5bundle exec rspec no ... Finished in 0.10174 seconds (files took 0.76083 seconds to load) 3 examples, 0 failures当写完测试代码通过了,功能就完成了,满足需求
之后更多需求就按步骤,改测试,改逻辑,运行成功
测试类型 ⇧
使用类型匹配器
Type matchersspec/requests/items_spec.rb
|
|
参考
4. 测试登录邮箱发送验证码 ⇧
登录流程
- 用户在页面填写邮箱,点击发送验证码 -> 用户收到验证码 -> 填写验证码提交 -> 验证登录查验证码表
- 目前测试的环节是 发送验证码
- 没有单独的请求去验证码对错,内部逻辑
- 需要在登录接口验证验证码
resources :session, only: [ :create, :destroy ] - 区别于验证
resources :validation_codes, only: [ :create ]是发送验证码
创建发送验证码接口测试文件 ⇧
使用命令创建
bin/rails generate rspec:request validation_codes
|
|
spec/requests/validation_codes_spec.rb
|
|
运行
bundle exec rspec1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16bundle exec rspec no ...F Failures: 1) ValidationCodes 验证码 可以被发送 Failure/Error: expect(response).to have_http_status(200) expected the response to have status code 200 but it was 202 # ./spec/requests/validation_codes_spec.rb:7:in `block (3 levels) in <main>' Finished in 0.10558 seconds (files took 0.80943 seconds to load) 4 examples, 1 failure Failed examples: rspec ./spec/requests/validation_codes_spec.rb:5 # ValidationCodes 验证码 可以被发送测试不通过
修改发送验证码接口 Controller ⇧
修改
app/controllers/api/v1/validation_codes_controller.rb
|
|
查看
ValidationCode表中的字段:db/schema.rb
|
|
修改
app/controllers/api/v1/validation_codes_controller.rb
|
|
- 需要构造一个随机数的验证码
使用 SecureRandom 构造随机数验证码 ⇧
搜索
rails generate random token
- rails generate random token
- rails generate 6 digit random token
- Dev Docs module SecureRandom
- Generating random number of length 6 with SecureRandom in Ruby
rails自带的方法 rials has_secure_token- 在对应的
model中写has_secure_token :code, length: 6 - 运行控制台测试代码
bin/rails console - 运行命令
bin/rails console,已经打开的需要输入reload!回车 Ctrl + D退出
- 在对应的
可使用的备选方案
|
|
app/controllers/api/v1/validation_codes_controller.rb
|
|
- 运行
bundle exec rspec测试成功 - 目前只是把验证码保存到数据库,并没有真正发送邮件
6. 内容回顾 ⇧
- 手动测试 api: 使用
curl、使用postman - 代码测试:
Rspec- 创建测试用例、配置连接测试环境数据库、构造数据
- 生成
modelrequest测试文件 - 完成两个测试:
Item、ValidationCodes - 创建数据、期待数据、发请求、验证状态码、期待响应内容
- 断言数据改变
change {...}.by 1 - 生成安全的随机数
使用到的命令
|
|
7. 补充内容 RSpec matcher Compound Expectations 复合期望 ⇧
- 多个逻辑判断 与或判断
- 搜索 RSpec matcher Compound Expectations
.and
- 使用
.and连接多个期待语句 - 也可以用
expect xx & xx
.or
- 使用
.or连接多个期待语句 - 也可以用
expect xx | xx | xx
参考文章
- rspec-rails
- rspec.info/
- Request specs
- Introduce RSpec Request Spec
- Testing in Rails: RSpec Tips and Tricks
- RSpec — Controller or Request Specs?
- ruby guides Action Mailer Basics
相关文章
- 代码
- Ruby ToolBox 可从各个方面评判库的活跃度
- 选 minitest 还是 rspec 请从各个方面对比优缺点
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名