使用 shoulda 重构 Rails 模型上的 rspec 测试 [英] Using shoulda to refactor rspec tests on Rails models

查看:31
本文介绍了使用 shoulda 重构 Rails 模型上的 rspec 测试的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

通过回答shoulda-matchers后stackoverflow.com/a/11849608/567863">另一个关于属性可访问性测试的 StackOverflow 问题(并认为它们非常棒),我决定尝试重构我在 Rails 教程 试图使它们更加简洁和彻底.我这样做要感谢模块文档的一些灵感 Shoulda::Matchers::ActiveRecord应该::Matchers::ActiveModel,以及这个 StackOverflow 答案关于在模型中构建 shoulda 测试.但是,仍有一些我不确定的事情,我想知道如何使这些测试变得更好.

After learning about shoulda-matchers by answering another StackOverflow question on attribute accessibility tests (and thinking they were pretty awesome), I decided to try refactoring the model tests I did in The Rails Tutorial in an attempt to make them even more concise and thorough. I did this thanks to some inspiration from the documentation for modules Shoulda::Matchers::ActiveRecord and Shoulda::Matchers::ActiveModel, as well as this StackOverflow answer on structuring shoulda tests in models. However, there's still a few things I am not sure about, and I am wondering how these tests could be made better.

我将使用 Rails 教程中的用户规范作为我的示例,因为它是最详细的,涵盖了许多可以改进的领域.以下代码示例已从原始 user_spec 更改.rb,并向下替换代码,直到 describe "micropost associations" 行.针对 user.rb 模型,其工厂定义在 工厂.rb.

I will use the User spec in the Rails Tutorial as my example as it is the most detailed, and covers lots of areas that could be improved. The following code example has been changed from the original user_spec.rb, and replaces the code down until the describe "micropost associations" line. The spec tests against the user.rb model, and its factory is defined in factories.rb.

spec/models/user_spec.rb

# == Schema Information
#
# Table name: users
#
#  id              :integer          not null, primary key
#  name            :string(255)
#  email           :string(255)
#  created_at      :datetime         not null
#  updated_at      :datetime         not null
#  password_digest :string(255)
#  remember_token  :string(255)
#  admin           :boolean          default(FALSE)
#
# Indexes
#
#  index_users_on_email           (email) UNIQUE
#  index_users_on_remember_token  (remember_token)
#

require 'spec_helper'

describe User do

  let(:user) { FactoryGirl.create(:user) }

  subject { user }

  describe "database schema" do
    it { should have_db_column(:id).of_type(:integer)
                              .with_options(null: false) }
    it { should have_db_column(:name).of_type(:string) }
    it { should have_db_column(:email).of_type(:string) }
    it { should have_db_column(:created_at).of_type(:datetime)
                              .with_options(null: false) }
    it { should have_db_column(:updated_at).of_type(:datetime)
                              .with_options(null: false) }
    it { should have_db_column(:password_digest).of_type(:string) }
    it { should have_db_column(:remember_token).of_type(:string) }
    it { should have_db_column(:admin).of_type(:boolean)
                              .with_options(default: false) }
    it { should have_db_index(:email).unique(true) }
    it { should have_db_index(:remember_token) }
  end

  describe "associations" do
    it { should have_many(:microposts).dependent(:destroy) }
    it { should have_many(:relationships).dependent(:destroy) }
    it { should have_many(:followed_users).through(:relationships) }
    it { should have_many(:reverse_relationships).class_name("Relationship")
                         .dependent(:destroy) }
    it { should have_many(:followers).through(:reverse_relationships) }
  end

  describe "model attributes" do
    it { should respond_to(:name) }
    it { should respond_to(:email) }
    it { should respond_to(:password_digest) }
    it { should respond_to(:remember_token) }
    it { should respond_to(:admin) }
    it { should respond_to(:microposts) }
    it { should respond_to(:relationships) }
    it { should respond_to(:followed_users) }
    it { should respond_to(:reverse_relationships) }
    it { should respond_to(:followers) }
  end

  describe "virtual attributes and methods from has_secure_password" do
    it { should respond_to(:password) }
    it { should respond_to(:password_confirmation) }
    it { should respond_to(:authenticate) }
  end

  describe "accessible attributes" do
    it { should_not allow_mass_assignment_of(:password_digest) }
    it { should_not allow_mass_assignment_of(:remember_token) }
    it { should_not allow_mass_assignment_of(:admin) }
  end

  describe "instance methods" do
    it { should respond_to(:feed) }
    it { should respond_to(:following?) }
    it { should respond_to(:follow!) }
    it { should respond_to(:unfollow!) }
  end

  describe "initial state" do
    it { should be_valid }
    it { should_not be_admin }
    its(:remember_token) { should_not be_blank }
    its(:email) { should_not =~ /\p{Upper}/ }
  end

  describe "validations" do
    context "for name" do
      it { should validate_presence_of(:name) }
      it { should_not allow_value(" ").for(:name) }
      it { should ensure_length_of(:name).is_at_most(50) }
    end

    context "for email" do
      it { should validate_presence_of(:email) }
      it { should_not allow_value(" ").for(:email) }
      it { should validate_uniqueness_of(:email).case_insensitive }

      context "when email format is invalid" do
        addresses = %w[user@foo,com user_at_foo.org example.user@foo.]
        addresses.each do |invalid_address|
          it { should_not allow_value(invalid_address).for(:email) }
        end
      end

      context "when email format is valid" do
        addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
        addresses.each do |valid_address|
          it { should allow_value(valid_address).for(:email) }
        end
      end
    end

    context "for password" do
      it { should ensure_length_of(:password).is_at_least(6) }
      it { should_not allow_value(" ").for(:password) }

      context "when password doesn't match confirmation" do
        it { should_not allow_value("mismatch").for(:password) }
      end
    end

    context "for password_confirmation" do
      it { should validate_presence_of(:password_confirmation) }
    end
  end

  # ...
end

关于这些测试的一些具体问题:

Some specific questions about these tests:

  1. 是否值得测试数据库架构?上面提到的 StackOverflow 答案 中的评论说:我只测试与行为相关的东西,我不考虑列或索引行为的存在.除非有人故意删除它们,否则数据库列不会消失,但您可以通过代码审查和信任来防止这种情况发生",我同意这一点,但是有什么正当理由说明将测试数据库模式,从而证明 Shoulda::Matchers::ActiveRecord 模块的存在是合理的?也许只是重要的指标值得测试......?
  2. "associations" 下的 should have_many 测试是否替换 "model attributes" 下相应的 should respond_to 测试>?我不知道 should have_many 测试是否只是在模型文件中查找相关的 has_many 声明,还是实际执行与 should respond_to.
  3. 您是否还有其他意见/建议使这些测试在内容和结构上更加简洁/可读/彻底?
  1. Is it worth testing the database schema at all? A comment in the StackOverflow answer mentioned above says "I only test things that are related to behavior and I don't consider the presence of a column or an index behavior. Database columns don't just disappear unless someone intentionally removes them, but you can protect against that with code reviews and trust", which I agree with, but is there any valid reason why the structure of the database schema would be tested for, and thus justifying the existence of the Shoulda::Matchers::ActiveRecord module? Perhaps just the important indexes are worth testing...?
  2. Do the should have_many tests under "associations" replace their corresponding should respond_to tests under "model attributes"? I can't tell whether the should have_many test just looks for the relevant has_many declaration in a model file or actually performs the same function as should respond_to.
  3. Do you have any other comments/suggestions to make these tests more concise/readable/thorough, both in content and structure?

推荐答案

1) Shoulda::Matchers::ActiveRecord 模块中包含的不仅仅是列和索引匹配器.我会在 包含的类中挖掘看看你能找到什么.这就是 have_manybelong_to 等的来源.不过,作为记录,我认为其中的大部分内容都没有什么价值.

1) The Shoulda::Matchers::ActiveRecord module has a lot more in it than just column and index matchers. I would dig around in the included classes a little and see what you can find. This is where the have_many, belong_to etc come from. For the record though, I see little value in most of what is in there.

2) 是的,诸如 have_many 之类的宏比模型是否响应方法要测试得多.来自 源代码,你可以准确地看到它正在测试什么:

2) Yes, macros such as have_many test a lot more than whether or not a model responds to a method. From the source code, you can see exactly what it is testing:

def matches?(subject)
  @subject = subject
  association_exists? &&
    macro_correct? &&
    foreign_key_exists? &&
    through_association_valid? &&
    dependent_correct? &&
    class_name_correct? &&
    order_correct? &&
    conditions_correct? &&
    join_table_exists? &&
    validate_correct?
end

3) 使测试更具可读性和/或简洁性绝对是一个需要回答的主观问题.每个人都会根据他们的背景和经验给你不同的答案.我个人会摆脱所有 respond_to 测试并用有价值的测试替换它们.当有人查看您的测试时,他们应该能够理解该类的公共 API.当我看到你的对象对诸如跟随?"之类的东西做出响应时,我可以做出假设,但并不真正知道这意味着什么.它需要争论吗?它是否返回布尔值?是物体跟随某物还是某物跟随物体?

3) Making the tests more readable and/or concise is definitely a subjective question to answer. Everyone will give you a different answer to this depending on their background and experience. I would personally get rid of all of the respond_to tests and replace them with tests that have value. When someone looks at your tests, they should be able to understand the public API for that class. When I see that your objects respond_to something like "following?", I can make assumptions, but don't really know what it means. Does it take an argument? Does it return a boolean value? Is the object following something or is something following the object?

这篇关于使用 shoulda 重构 Rails 模型上的 rspec 测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆