如何解决测试控制器正确处理唯一性验证的正确方法是什么?
摘要
我正在构建一个包含用户注册过程的Rails应用。 username
和password
对于在数据库中创建user
对象是必需的; username
必须是唯一的。 我正在寻找正确的方法来测试唯一性验证是否提示了控制器方法的特定操作,即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
模型的规格文件使用shoulda-matchers测试了此验证:
# 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
自从User
唯一性规范通过以来,我知道一个username
请求(其中已经包含数据库中的一个POST
)导致username
进入条件的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
和before
钩子。如果没有after
,该测试在将来运行时将失败:无法创建新记录,因此使用相同的User.last.destroy
创建 second 记录不会不会发生。
问题
我应该在控制器规格中测试这种特定的模型验证吗?如果是这样,这些挂钩是实现此目标的正确/最佳方法吗?
解决方法
我会避免对“我应该……”这一部分发表意见,但是有几个方面值得考虑。首先,尽管尚未正式弃用控制器测试,但Rails和Rspec团队通常都不建议使用它们。从RSpec 3.5 release notes:
Rails团队和RSpec核心团队的官方推荐 是写请求规范。要求规格可让您专注于 单个控制器动作,但与控制器测试不同的是, 路由器,中间件堆栈以及机架请求和响应。 这为您编写的测试增加了真实感,并有助于避免 控制器规格中常见的许多问题。
该方案是否需要相应的请求规范是一个判断调用,但是如果您想在模型级别对验证进行单元测试,请查看shoulda matchers gem,这有助于进行模型验证测试。
关于钩子的问题,before(:all)
钩子在数据库事务之外运行,因此即使您在RSpec配置中将use_transactional_fixtures
设置为true
,它们也不会自动回滚。因此,匹配的after(:all)
是您要走的路。替代方法包括:
- 在
before(:each)
挂钩内创建用户,该挂钩确实在事务中运行并回滚。这是以某些测试性能为代价的。 - 使用Database Cleaner 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!
来设置特定示例的给定,因为它的优点是您可以通过示例所生成的辅助方法在示例中引用它们。除了诸如扎根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."
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。