11

在通过回答关于属性可访问性测试的另一个 StackOverflow 问题(并认为它们非常棒)了解了shoulda-matchers之后,我决定尝试重构我在Rails 教程中所做的模型测试,以使它们更加简洁和彻底。我之所以这样做,是因为模块文档和. 但是,仍有一些事情我不确定,我想知道如何才能使这些测试变得更好。 Shoulda::Matchers::ActiveRecordShoulda::Matchers::ActiveModel

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

规格/模型/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

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

  1. 是否值得测试数据库模式?上面提到的 StackOverflow 答案中的一条评论说“我只测试与行为相关的东西,我不考虑列或索引行为的存在。除非有人故意删除它们,否则数据库列不会消失,但是你可以通过代码审查和信任来防止这种情况”,我同意这一点,但是是否有任何正当理由可以测试数据库模式的结构,从而证明 Shoulda::Matchers::ActiveRecord模块的存在是合理的?也许只有重要的指标值得测试......?
  2. 下的should have_many测试是否"associations"替换should respond_to下相应的测试"model attributes"?我无法判断should have_many测试是只是在模型文件中查找相关has_many声明,还是实际上执行与should respond_to.
  3. 您是否有任何其他意见/建议可以使这些测试在内容和结构上更简洁/可读/彻底?
4

5 回答 5

4

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

2)是的,诸如have_many测试之类的宏比模型是否响应方法要多得多。从源代码中,您可以确切地看到它正在测试什么:

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。当我看到您的对象对“跟随?”之类的内容做出响应时,我可以做出假设,但并不真正知道这意味着什么。需要争论吗?它是否返回布尔值?对象是跟随某物还是某物跟随该对象?

于 2012-09-03T00:34:17.283 回答
1

你的问题涉及到几点,我想谈谈其中两个:

答案是主观的,所以我会给你我个人的看法。

1) 以这种方式测试 ActiveRecord?
我的回答是肯定的。你可以用真实数据编写复杂的测试,但如果你基本上信任 ActiveRecord,你可以这样做,如果你开始做 tdd,首先通过这些测试,它们可以在这个过程中提供帮助。

2) 编写模型测试?
我的回答是肯定的。我所做的是关注控制器并在快乐路径上请求规范,然后对于需要验证等的情况,我为它们编写单元模型测试。事实证明,这对我来说是一个很好的责任分工。

于 2012-09-03T00:54:11.837 回答
0

我认为这整件事应该从规范的角度来看。

如果您有一个组件测试级别的规范,它涵盖了给定模型的必要数据库列,那么您应该这样做,否则不应该这样做。

如果没有涵盖,但作为一个负责任的开发人员,你觉得拥有它很重要(你的软件和它的质量特性更好),你必须安排在规范中包含这些信息,然后你可以把这些测试放在测试套件中.

于 2012-08-24T05:54:46.907 回答
0

较低的测试级别的要求主要来自您的组织内部(内部文档),客户大多只提供客户需求规范(假设这是测试 V 模型的最高级别)。当您的组织开始设计时,软件会逐步创建较低的测试级别规范。

对于“我们真的需要这个”问题:这取决于很多因素:应用程序的复杂性、安全性与否、要遵循的标准、合同/法律/行业法规等。

一般来说,我会说,对于一个正确的理想应用程序,负责单元测试的需求应该编写单元级规范,测试人员应该根据这个规范实施测试。

对于“have_many 和 respond_to”,恐怕我没有背景信息它们是如何实现的,所以无法回答。

于 2012-08-29T14:07:21.393 回答
0

我发现为数据库列的存在编写测试有一些价值。原因如下:

1) 编写它们让我保持在 TDD 的节奏中。
2)迁移通常非常棒,直到它们不是。我知道您不应该编辑现有的迁移,但是当我自己做某事时,我有时还是会这样做。如果其他人正在开发同一个应用程序并更改现有迁移而不是编写新迁移,那么这些测试对我来说很快就会隔离问题。

如果您陷入了太多列名和类型的困扰,您可以执行以下操作来节省自己的输入:

describe User do

  describe 'database' do 

    describe 'columns' do 

      %w[reset_password_sent_at remember_created_at current_sign_in_at 
        last_sign_in_at confirmed_at confirmation_sent_at 
        created_at updated_at
        ].each do |column|
        it { should have_db_column(column.to_sym).of_type(:datetime) }
      end
    end

    describe 'indexes' do 

      %w[confirmation_token email reset_password_token
      ].each do |index|
        it { should have_db_index(index.to_sym).unique(true)}
      end
    end
  end  
end

希望有帮助。

于 2013-01-15T03:50:38.603 回答