测试控制器是否适当处理唯一性验证的正确方法是什么?
What is the proper way to test that a controller appropriately handles a uniqueness validation?
总结
我正在构建一个包含用户注册过程的 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
自从 username
唯一性的 User
规范通过后,我 知道 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 记录相同的 username
不会发生。
问题
我应该在控制器规范中测试这个特定的模型验证吗?如果是这样,这些挂钩是 right/best 实现此目标的方法吗?
我将避开对 'should I...' 部分的意见,但有几个方面值得考虑。首先,虽然控制器测试还没有被正式弃用,但一段时间以来 Rails 和 Rspec 团队普遍不鼓励使用它们。来自 RSpec 3.5 release notes:
The official recommendation of the Rails team and the RSpec core team
is to write request specs instead. Request specs allow you to focus on
a single controller action, but unlike controller tests involve the
router, the middleware stack, and both rack requests and responses.
This adds realism to the test that you are writing, and helps avoid
many of the issues that are common in controller specs.
场景是否保证相应的请求规范是一个明智的gement 调用,但如果您想在模型级别对验证进行单元测试,请查看 shoulda matchers gem,这有助于模型验证测试)。
就你关于挂钩的问题而言,before(:all)
在数据库事务之外挂钩 运行,所以即使你在 [=] 中将 use_transactional_fixtures
设置为 true
37=] 配置,它们不会自动回滚。所以,匹配 after(:all)
就像你拥有的那样。备选方案包括:
- 在
before(:each)
挂钩中创建用户,它在事务中执行 运行 并被回滚。这是一些测试性能的潜在成本。
- 使用像 Database Cleaner gem 这样的工具,它可以让您 fine-grained 控制清理测试数据库的策略。
如果您想涵盖控制器以及用户反馈方面的内容,我会建议功能规范:
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,因为它的优点是您可以通过它生成的辅助方法在示例中引用它们。 before(:all)
通常应避免使用,除了诸如删除 API 之类的东西。每个示例都应该有自己的 setup/teardown.
但是你还需要处理控制器本身坏掉的情况。它应该是:
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 实际上指向新资源。如果您不重新加载页面,则会加载索引。
这也消除了在会话中填充错误消息的需要。如果您想通过 Flash 提供有用的反馈,您可以这样做:
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."
总结
我正在构建一个包含用户注册过程的 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
自从 username
唯一性的 User
规范通过后,我 知道 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 记录相同的 username
不会发生。
问题
我应该在控制器规范中测试这个特定的模型验证吗?如果是这样,这些挂钩是 right/best 实现此目标的方法吗?
我将避开对 'should I...' 部分的意见,但有几个方面值得考虑。首先,虽然控制器测试还没有被正式弃用,但一段时间以来 Rails 和 Rspec 团队普遍不鼓励使用它们。来自 RSpec 3.5 release notes:
The official recommendation of the Rails team and the RSpec core team is to write request specs instead. Request specs allow you to focus on a single controller action, but unlike controller tests involve the router, the middleware stack, and both rack requests and responses. This adds realism to the test that you are writing, and helps avoid many of the issues that are common in controller specs.
场景是否保证相应的请求规范是一个明智的gement 调用,但如果您想在模型级别对验证进行单元测试,请查看 shoulda matchers gem,这有助于模型验证测试)。
就你关于挂钩的问题而言,before(:all)
在数据库事务之外挂钩 运行,所以即使你在 [=] 中将 use_transactional_fixtures
设置为 true
37=] 配置,它们不会自动回滚。所以,匹配 after(:all)
就像你拥有的那样。备选方案包括:
- 在
before(:each)
挂钩中创建用户,它在事务中执行 运行 并被回滚。这是一些测试性能的潜在成本。 - 使用像 Database Cleaner gem 这样的工具,它可以让您 fine-grained 控制清理测试数据库的策略。
如果您想涵盖控制器以及用户反馈方面的内容,我会建议功能规范:
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,因为它的优点是您可以通过它生成的辅助方法在示例中引用它们。 before(:all)
通常应避免使用,除了诸如删除 API 之类的东西。每个示例都应该有自己的 setup/teardown.
但是你还需要处理控制器本身坏掉的情况。它应该是:
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 实际上指向新资源。如果您不重新加载页面,则会加载索引。
这也消除了在会话中填充错误消息的需要。如果您想通过 Flash 提供有用的反馈,您可以这样做:
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."