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

问题描述

摘要

我正在构建一个包含用户注册过程的Rails应用。 usernamepassword对于在数据库中创建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)是您要走的路。替代方法包括:

  1. before(:each)挂钩内创建用户,该挂钩确实在事务中运行并回滚。这是以某些测试性能为代价的。
  2. 使用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."

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...