测试控制器正确处理唯一性验证的正确方法是什么



摘要

我正在构建一个Rails应用程序,其中包括一个用户注册过程。usernamepassword是在数据库中创建user对象所必需的;CCD_ 4必须是唯一的我正在寻找正确的方法来测试唯一性验证是否会提示控制器方法的特定操作,即UsersController#create

上下文

用户模型包括相关验证:

# app/models/user.rb
#
#  username        :string           not null
#  ...
class User < ApplicationRecord
validates :username, presence: true
# ... more validations, class methods, and instance methods
end

此外,User模型的规范文件使用should匹配器:来测试这种验证

# spec/models/user_spec.rb
RSpec.describe User, type: :model do
it { should validate_uniqueness_of(:username)}
# ... more model tests
end

方法UsersController#create定义如下:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
render :show
else
flash[:errors] = @user.errors.full_messages
redirect_to new_user_url
end
end
# ... more controller methods
end

由于username唯一性的User规范通过,我知道数据库中已经包含usernamePOST请求将导致UsersController#create进入条件的else部分,我希望通过测试来验证这种情况。

目前,我测试UsersController#create如何以以下方式处理username上的唯一性验证:

# spec/controllers/users_controller_spec.rb
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
describe 'POST #create' do
context "username exists in db" do
before(:all) do
User.create!(username: 'jarmo', password: 'good_password')
end
before(:each) do
post :create, params: { user: { username: 'jarmo', password: 'better_password' }}
end
after(:all) do
User.last.destroy
end
it "redirects to new_user_url" do
expect(response).to redirect_to new_user_url
end
it "sets flash errors" do
should set_flash[:errors]
end
end
# ... more controller tests
end

问题

我主要关心的是beforeafter挂钩。如果没有User.last.destroy,该测试在将来运行时将失败:无法创建新记录,因此不会创建具有相同username第二个记录。

问题

我应该在控制器规范中测试这个特定的模型验证吗?如果是,这些挂钩是实现这一目标的正确/最佳方式吗?

我会回避关于"我应该……"部分的意见,但有几个方面值得考虑。首先,尽管控制器测试还没有被正式否决,但Rails和Rspec团队已经普遍不鼓励控制器测试了一段时间。来自RSpec 3.5发行说明:

Rails团队和RSpec核心团队的官方推荐而是编写请求规范。请求规格允许您专注于单个控制器操作,但与控制器测试不同的是路由器、中间件堆栈以及机架请求和响应。这为你所写的测试增加了真实感,并有助于避免控制器规格中常见的许多问题。

场景是否保证相应的请求规范是一个判断调用,但如果您想在模型级别对验证进行单元测试,请查看should a matchers gem,它有助于模型验证测试(。

就关于钩子的问题而言,before(:all)钩子在数据库事务之外运行,因此即使在RSpec配置中将use_transactional_fixtures设置为true,它们也不会自动回滚。所以,一个像你一样匹配的after(:all)是一条路。备选方案包括:

  1. before(:each)钩子内创建用户,该钩子在事务中运行并回滚。这是以一些测试性能为潜在代价的
  2. 使用像数据库清理器gem这样的工具,它可以对清理测试数据库的策略进行细粒度的控制

如果你想把控制器和用户反馈放在一起,我建议你使用一个功能规范:

RSpec.feature "User creation" do
context "with duplicate emails" do
let!(:user) { User.create!(username: 'jarmo', password: 'good_password') }
it "does not allow duplicate emails" do
visit new_user_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'p4ssw0rd'
fill_in 'Password Confirmation', with: 'p4ssw0rd'
expect do
click_button 'Sign up'
end.to_not change(User, :count)
expect(page).to have_content 'Email has already been taken'
end
end
end

这不是戳控制器内部,而是从用户故事中驱动整个堆栈,并测试视图是否也有验证错误的输出——因此,在控制器规范提供的值很少的情况下,它提供了值。

使用let/let!为特定示例设置givens,因为它的优点是可以通过它生成的helper方法在示例中引用它们。除了剔除API之外,一般应避免使用before(:all)。每个示例都应该有自己的设置/拆卸。

但您也需要处理控制器本身已损坏的事实。它应该是:

class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new, status: :unprocessable_entity
end
end
end

当记录无效时,不应重定向回。在显示执行POST请求的结果时,再次呈现表单。重定向回将导致可怕的用户体验,因为所有字段都将被清空。

创建资源成功后,应将用户重定向到新创建的资源,以便浏览器URL实际指向新资源。如果不重新加载页面,则会加载索引。

这也消除了在会话中填充错误消息的需要。如果你想通过闪光灯提供有用的反馈,你可以这样做:

class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
flash.now[:error] = "Signup failed."
render :new, status: :unprocessable_entity
end
end
end

你可以用测试它

expect(page).to have_content "Signup failed."

相关内容