【山竹记账后端】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 的弊端

  1. 难写
  2. 难批量
  3. 难重复操作

使用 postman 的弊端

  1. Collection中,单独写自动化测试断言和脚本、录入接口再执行;
  2. 与源代码分离,逻辑分散不内聚
  3. 团队协作体验差,免费版有团队人数和分享额度限制,高级功能收费昂贵,自定义定制难
  4. 复杂业务流控死板、复杂的业务逻辑,不如直接编写代码灵活

使用纯代码框架可以解决以上困难,请问哪里找好用的测试框架?

RSpec

评判一个库,需要关注哪些方面

  • 最近三年的活跃程度
  • 是否有长期的更新
  • 更新年份的分布(RSpecRails 一起成长,早于 Github 成立)
  • 下载数、forks数、issue关闭数关闭率、PR接受率 等
  • star 数(不一定,monorepo 项目库分流;是否早于 Github 成立,GithubRails写的)

RSpec比Rails自带的minitest更爽一点,更像是 DSL,符合 Spec 风格(describe/it) BDD 风格

0.单元测试要测什么

  • 目前大都只测试 Controllers
    • 例如 登录 jwt
  • 因为目前项目的 ModelsViews 都很简单
    • json 字段格式类型是否符合
    • 数据数量、字段等

不测哪些

  • 不测 Rails 自带的功能,因为 Rails 测过了
    • 比如 validate 功能本身
  • 不测 第三方功能,因为他们应该自己测,直接 mock
    • 比如发邮件功能,邮件是否送达

1. 安装 RSpec

打开 Gemfile,将 gem 'rspec-rails', '~> 8.0.0'复制到 group :development, :test do

1
2
3
4
5
#...
group :development, :test do
  gem 'rspec-rails', '~> 8.0.0'
  #...
end
  • 将其添加到 :development 组中并不是必须的,
    • 但如果没有这样做,那么生成器和回收任务就必须以 RAILS_ENV=test 作为前缀来标识。
  • 然后运行 bundle install --verbosebundle --verbose
    • 可以切换国内源,加速安装

初始化 RSpec,生成项目依赖引入入口文件

  • 运行 bin/rails generate rspec:install,自动创建几个帮助文件
    • .rspec 依赖 spec_helper 帮助方法
    • spec/
    • spec/rails_helper.rb
    • spec/spec_helper.rb

2. 使用测试数据库

从初始测试用例开始:user 演示

  • 由于之前已经创建过了 bin/rails generate model user
  • 可以直接初始话对应的测试文件,运行 bin/rails generate rspec:model user

创建了 spec/models/user_spec.rb

1
2
3
4
5
require 'rails_helper'

RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end
  • require 'rails_helper' 引入 spec_helper 帮助方法
  • RSpec.describe User BDD 风格描述,接受两个参数:
    • type: :model 指定类型
    • 代码块 do ... end

运行 bundle exec rspec 报错

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
bundle exec rspec

An error occurred while loading ./spec/models/user_spec.rb.
Failure/Error: ActiveRecord::Migration.maintain_test_schema!

ActiveRecord::ConnectionNotEstablished:
  connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: No such file or directory
        Is the server running locally and accepting connections on that socket?
  connection to server on socket "/run/postgresql/.s.PGSQL.5432" failed: No such file or directory
        Is the server running locally and accepting connections on that socket?
  connection to server on socket "/tmp/.s.PGSQL.5432" failed: No such file or directory
        Is the server running locally and accepting connections on that socket?
...
  • 提示未连接数据库,在外部系统命令中启动 docker start db-for-mangosteen

运行 bundle exec rspec 还是报错

  • 是由于目前只配置了开发环境数据库,测试环境数据库未配置

配置测试数据库

config/database.yml 中配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: mangosteen_dev
  username: mangosteen
  password: 123456
  host: db-for-mangosteen

test:
  <<: *default
  database: mangosteen_1_test
  username: mangosteen
  password: 123456
  host: db-for-mangosteen

production:
  <<: *default
  database: mangosteen_1_production
  username: mangosteen_1
  password: <%= ENV["MANGOSTEEN_1_DATABASE_PASSWORD"] %>

创建测试数据库

  1. 运行 docker exec -it db-for-mangosteen bash 打开终端创建,步骤复杂 exit
  2. 使用 Rails 创建,运行命令:RAILS_ENV=test bin/rails db:create

    1
    2
    
    RAILS_ENV=test bin/rails db:create
    # Created database 'mangosteen_1_test'

迁移数据库创建表

  • 运行命令 RAILS_ENV=test bin/rails db:migrate

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    RAILS_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

1
2
3
4
5
6
7
8
9
require 'rails_helper'

RSpec.describe User, type: :model do
  it '有 email' do
    user = User.create email: 'frank@1.com'
    expect(user).to be 'frank@1.com'
  end
  
end

运行测试启动命令 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
23
24
25
26
27
28
29
30
31
32
33
34
35
bundle exec rspec                              no
F

Failures:

  1) User  email
     Failure/Error: expect(user).to be 'frank@1.com'
     
       expected #<String:13480> => "frank@1.com"
            got #<User:13460> => #<User id: 1, email: [FILTERED], name: nil, created_at: "2026-09-11 07:15:55.570897000 +0000", updated_at: "2026-09-11 07:15:55.570897000 +0000">
     
       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       `expect(actual).to eq(expected)` if you don't care about
       object identity in this example.
     
     
       Diff:
       @@ -1 +1,6 @@
       -"frank@1.com"
       +#<User:0x00007a7ca070fcc8
       + id: 1,
       + email: [FILTERED],
       + name: nil,
       + created_at: "2026-09-11 07:15:55.570897000 +0000",
       + updated_at: "2026-09-11 07:15:55.570897000 +0000">
       
     # ./spec/models/user_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.06264 seconds (files took 2.72 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/models/user_spec.rb:4 # User 有 email
  • 看报错提示使用 eq 修改对象属性 user.email

    • to be 比较两个对象 是否完全相同,不适合

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      require '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

创建测试用例,并验证

由于使用的时 railsapi 模式,目前只是用 RSpecrequest test 功能(暂不使用其 controller test

  • 运行命令 bin/rails generate rspec:request items 创建 约定的 测试文件

    1
    2
    
    bin/rails generate rspec:request items
    # create  spec/requests/items_spec.rb

spec/requests/items_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "GET /items" do
    it "works! (now write some real specs)" do
      get items_index_path
      expect(response).to have_http_status(200)
    end
  end
end
  • RSpec.describe "Items", type: :request 接受两个参数:
    • type: :request 指定类型
    • 代码块 do ... end

查看路由 config/routes.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :validation_codes, only: [:create]
      resources :session, only: [:create, :destroy]
      resources :me, only: [:show]
      resources :items, exclude: [:put]
      resources :tags, exclude: [:put]
    end
  end

end
  • 判断 items 的各个请求方法都有
  • 直接运行测试命令 bundle exec rspec

测试未通过,报错

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.F

Failures:

  1) Items GET /items works! (now write some real specs)
     Failure/Error: get items_index_path
     
     NameError:
       undefined local variable or method `items_index_path' for #<RSpec::ExampleGroups::Items::GETItems "works! (now write some real specs)" (./spec/requests/items_spec.rb:5)>
     # ./spec/requests/items_spec.rb:6:in `block (3 levels) in <main>'

Finished in 0.03405 seconds (files took 0.9974 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)
  • 提示找不到 items_index_path,修改为真实url ‘/api/v1/items’

spec/requests/items_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "GET /items" do
    it "works! (now write some real specs)" do
      get '/api/v1/items'
      expect(response).to have_http_status(200)
    end
  end
end
  • 运行成功

验证是否改了 app/controllers/api/v1/items_controller.rb 后测试失败

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Api::V1::ItemsController < ApplicationController
  def index
    # item = Item.page(params[:page]).per(100)
    item = Item.page params[:page]
    render json: {
      resource: item,
      pager: {
        page: params[:page],
        per_page: 100,
        count: Item.count
      }
    },
    status: 201
  end

  def create
    item = Item.new amount: 1
    if item.save
      render json: { resource: item }
    else
      render json: { error: item.errors }
    end
  end
end
  • 修改返回参数 status: 201
  • 再运行测试 bundle exec rspec

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    bundle 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

参考


构造测试数据

spec/requests/items_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "GET /items" do
    it "works! (now write some real specs)" do
      11.times do
        Item.new amount: 100
      end
      expect(Item.count).to eq(11)  
      get '/api/v1/items'
      expect(response).to have_http_status(200)
      json = JSON.parser(response.body)
      expect(json['resources'].size).to eq(10)
    end
  end
end
  • 构造11项数据 11.times do ... end
    • 构造一个对象 Item.new amount: 100
  • 期待 Item.count 的数量为 11
  • 发请求,期待 response.status200
  • 解析json,期待 resources['resources'].size10
  • 运行 bundle exec rspec,看报错

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    bundle 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "GET /items" do
    it "works! (now write some real specs)" do
      11.times do
        Item.new amount: 100
      end
      p "11.times------------------"
      p Item
      p "------------------11.times"
      expect(Item.count).to eq(11)  
      get '/api/v1/items'
      expect(response).to have_http_status(200)
      p "response------------------"
      p response.body
      p "------------------response"
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq(10)
    end
  end
end
  • 运行 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
    23
    
    bundle 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
    5
    
    bundle exec rspec                             no
    ..
    
    Finished in 0.10114 seconds (files took 0.77976 seconds to load)
    2 examples, 0 failures
  • 一句逻辑,一句断言

测试第二页 spec/requests/items_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "index by page" do
    it "works! (now write some real specs)" do
      11.times do
        Item.create amount: 100
      end
      expect(Item.count).to eq(11)
      get '/api/v1/items'
      expect(response).to have_http_status(200)
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq(10)

      get '/api/v1/items?page=2'
      expect(response).to have_http_status(200)
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq(1)
    end
  end
end

测试如何确定创建Item成功

创建Item成功的确凿证据时数据库变了 spec/requests/items_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "index by page" do
    it "验证了共11条数据,每页10条" do
      #...
    end
  end

  describe "create" do
    it "can create a item" do
      expect(Item.count).to eq(0)
      post '/api/v1/items', params: { amount: 99 }
      expect(Item.count).to eq(1)
    end
  end
end
  • 每个测试用力之间不应该有任何干扰,这样移动任何一个用力顺序的时候,就完全不会受影响
  • rails rspec 只要运行完一个测试用例,会自动清空数据(保留表),下次再重新创建
  • 运行 bundle exec rspec

    1
    2
    3
    4
    
    ...
    
    Finished in 0.10483 seconds (files took 0.82176 seconds to load)
    3 examples, 0 failures

尝试运行失败的用例 spec/requests/items_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
require '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(Item.count).to eq(0)
      post '/api/v1/items', params: { amount: 99 }
      expect(Item.count).to eq(2)
    end
  end
end
  • 运行 bundle exec rspec

     1
     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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
require '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 }.from(0).to(1)
    end
  end
end
  • 运行 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
      18
      
      require '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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
require '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)
      # expect(response).to have_http_status(201)
      expect(response).to have_http_status(200)
      json = JSON.parse(response.body)
      expect(json['resource']['amount']).to eq(99)
    end
  end
end
  • 成功运行一个测视用例后,一定要尝试修改用例是否会报错,会报错说明测试代码正确处理
  • 类似.to eq(99) 的最后一个括号可以不写,看上去更像英文
  • 但中间的括号必须写,因为需要链式调用

spec/requests/items_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
require '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
      expect(response).to have_http_status 200
      json = JSON.parse response.body
      expect(json['resource']['amount']).to eq 99
    end
  end
end
  • 这个用例做了:
    • 传参发请求
    • 验证数据库数据
    • 验证状态码
    • 解析 json
    • 解析 json 中字段,验证
  • 运行 bundle exec rspec 查看报错

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    bundle 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Api::V1::ItemsController < ApplicationController
  def index
    # ...
  end

  def create
    item = Item.new amount: params[:amount]
    if item.save
      render json: { resource: item }
    else
      render json: { error: item.errors }
    end
  end
end

如果有多个参数,需要每个都写出来吗? 例如: amount: params[:amount], note: params[:note], ...

  • 后面处理,ruby 动态语言已经有成熟的处理方法;java 就必须都一一写出来
  • 运行 bundle exec rspec 查看报错

    1
    2
    3
    4
    5
    
    bundle exec rspec                                                     no
    ...
    
    Finished in 0.10174 seconds (files took 0.76083 seconds to load)
    3 examples, 0 failures
  • 当写完测试代码通过了,功能就完成了,满足需求

  • 之后更多需求就按步骤,改测试,改逻辑,运行成功


测试类型

使用类型匹配器 Type matchers spec/requests/items_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
require '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
      expect(response).to have_http_status 200
      json = JSON.parse response.body
      expect(json['resource']['id']).to be_an(Numeric)
      expect(json['resource']['amount']).to eq 99
    end
  end
end

参考


4. 测试登录邮箱发送验证码

登录流程

  • 用户在页面填写邮箱,点击发送验证码 -> 用户收到验证码 -> 填写验证码提交 -> 验证登录查验证码表
  • 目前测试的环节是 发送验证码
    • 没有单独的请求去验证码对错,内部逻辑
  • 需要在登录接口验证验证码 resources :session, only: [ :create, :destroy ]
  • 区别于验证 resources :validation_codes, only: [ :create ] 是发送验证码

创建发送验证码接口测试文件

使用命令创建 bin/rails generate rspec:request validation_codes

1
2
bin/rails generate rspec:request validation_codes                                   no
#      create  spec/requests/validation_codes_spec.rb

spec/requests/validation_codes_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
require 'rails_helper'

RSpec.describe "ValidationCodes", type: :request do
  describe "验证码" do
    it "可以被发送" do
      post '/api/v1/validation_codes', params: { email: 'your_email@163.com' }
      expect(response).to have_http_status(200)
    end
  end
end
  • 运行 bundle exec rspec

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    bundle 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

1
2
3
4
5
6
class Api::V1::ValidationCodesController < ApplicationController
  def create
    validation_code = ValidationCode.new email: params[:email]
    params[:email]
  end
end

查看ValidationCode表中的字段:db/schema.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ActiveRecord::Schema[7.2].define(version: 2026_09_10_063034) do
  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "items", force: :cascade do |t|
    t.bigint "user_id"
    t.integer "amount"
    t.text "note"
    t.bigint "tags_id", array: true
    t.datetime "happened_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.string "email"
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "validation_codes", force: :cascade do |t|
    t.string "email"
    t.integer "kind", default: 1, null: false
    t.string "code", limit: 100
    t.datetime "used_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end

修改 app/controllers/api/v1/validation_codes_controller.rb

1
2
3
4
5
6
7
class Api::V1::ValidationCodesController < ApplicationController
  def create
    validation_code = ValidationCode.new email: params[:email],
      kind: "sign_in", code: "<随机数>"
    params[:email]
  end
end
  • 需要构造一个随机数的验证码

使用 SecureRandom 构造随机数验证码

搜索 rails generate random token

可使用的备选方案

1
2
SecureRandom.random_number(100_000..999_999).to_s
SecureRandom.random_number.to_s[2..7]

app/controllers/api/v1/validation_codes_controller.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Api::V1::ValidationCodesController < ApplicationController
  def create
    code = SecureRandom.random_number.to_s[2..7]
    validation_code = ValidationCode.new email: params[:email],
      kind: "sign_in", code: code
    if validation_code.save
      head 200
    else
      render json: { errors: validation_code.errors }
    end
  end
end
  • 运行 bundle exec rspec 测试成功
  • 目前只是把验证码保存到数据库,并没有真正发送邮件

6. 内容回顾

  • 手动测试 api: 使用 curl、使用 postman
  • 代码测试:Rspec
    • 创建测试用例、配置连接测试环境数据库、构造数据
    • 生成 model request 测试文件
    • 完成两个测试:ItemValidationCodes
    • 创建数据、期待数据、发请求、验证状态码、期待响应内容
    • 断言数据改变 change {...}.by 1
    • 生成安全的随机数

使用到的命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 创建 约定的 测试帮助依赖
bin/rails generate rspec:install
# 测试 model
# 初始生成 model 包含测试文件
bin/rails generate model user
# 已有 model,单独生成测试文件 创建 约定的 测试文件(已配置 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
# 测试 request
# 生成请求测试文件
bin/rails generate rspec:request items

7. 补充内容 RSpec matcher Compound Expectations 复合期望

.and

  • 使用 .and 连接多个期待语句
  • 也可以用 expect xx & xx

.or

  • 使用 .or 连接多个期待语句
  • 也可以用 expect xx | xx | xx

·未完待续·

参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名