如何用RSpec测试带JWT认证的Ruby on Rails API控制器?
问题背景
我使用rspec-rails对Ruby on Rails(RoR)的API控制器进行单元测试,所有控制器均继承自V1::ProtectedController,该类通过拦截请求并验证Authorization请求头实现JWT认证,核心逻辑如下:
class V1::ProtectedController < V1::BaseController include JsonWebTokenAuthentication attr_reader :current_user before_action :validate_token! after_action :verify_authorized private def validate_token! authorization_header = request.headers['Authorization'] || '' puts "header: #{request.headers['Authorization']}" jwt = JsonWebToken.new(authorization_header.gsub('Bearer ', '')) if payload = jwt.verify account = Account.find_by(id: payload[:user][:id]) if account.nil? || account.token_checksum == payload[:user][:token_checksum] @current_user = Account.from_jwt_data(payload[:user]) end end return if @current_user raise JsonWebTokenAuthentication::Unauthorized, 'invalid token' end end
为模拟认证流程,我编写了AuthHelper模块获取管理员JWT令牌:
module AuthHelper include Rack::Test::Methods def app Rails.application end def admin_login params = { grant_type: 'password', email: 'admin@arime.com', password: 'preci666' } post '/api/v1/auth/token', params expect(last_response.status).to eq(200) auth_response = JSON.parse(last_response.body) @access_token = auth_response['access_token'] end end
但在Workers控制器测试中,携带令牌的请求始终返回401状态码:
- 调试发现
ProtectedController中request.headers['Authorization']为空,但request.inspect能看到该请求头 - 尝试直接赋值
request.headers['Authorization'] = auth时,触发错误:
1) V1::WorkersController GET #index assigns @workers Failure/Error: request.headers['Authorization'] = auth ArgumentError: wrong number of arguments (given 0, expected 1..2)
环境版本:Ruby 2.7.7、Rails 7.0.4.2、rspec-rails 6.0.1
解决方案
方案一:修复请求头传递问题
在Rails 7+的RSpec控制器测试中,request对象的headers操作方式发生变化,不能直接使用request.headers['key'] = value,需改用以下方式:
request.headers.merge!('Authorization' => "Bearer #{@access_token}")
完整测试用例示例:
RSpec.describe V1::WorkersController, type: :controller do include AuthHelper before do admin_login request.headers.merge!('Authorization' => "Bearer #{@access_token}") end describe 'GET #index' do it 'assigns @workers' do get :index expect(response).to have_http_status(:ok) # 添加其他业务断言 end end end
如果仍出现request.headers['Authorization']为空的情况,可直接操作请求env:
request.env['HTTP_AUTHORIZATION'] = "Bearer #{@access_token}"
方案二:更优的模拟认证方案(绕过真实JWT验证)
直接在测试中模拟current_user,跳过真实的JWT解析与验证流程,提升测试速度与稳定性:
- 修改
ProtectedController,添加测试环境的跳过逻辑:
class V1::ProtectedController < V1::BaseController # 原有代码不变 private def validate_token! # 测试环境下直接使用预设的current_user return if Rails.env.test? && @current_user.present? # 原有JWT验证逻辑... end end
- 更新
AuthHelper,添加模拟登录方法:
module AuthHelper def sign_in_as_admin # 优先查找已有管理员,无则创建(假设使用FactoryBot生成测试数据) @current_user = Account.find_by(email: 'admin@arime.com') || create(:admin_account) controller.instance_variable_set(:@current_user, @current_user) end end
- 在测试用例中使用模拟登录:
RSpec.describe V1::WorkersController, type: :controller do include AuthHelper before do sign_in_as_admin end describe 'GET #index' do it 'assigns @workers' do get :index expect(response).to have_http_status(:ok) # 添加其他业务断言 end end end
这种方式无需调用真实认证接口,也不用处理JWT传递问题,测试效率更高,同时避免了对外部认证服务的依赖。
内容的提问来源于stack exchange,提问作者Safouane Jelassi
相关产品推荐
相关产品推荐

