使用shoulda重构Rails模型上的rspec测试

通过回答关于属性可访问性测试的另一个StackOverflow问题 (并认为它们非常棒)来了解shoulda-matchers后,我决定尝试重构我在The Rails Tutorial中所做的模型测试,试图使它们更加简洁和彻底。 我这样做归功于来自模块的文档的一些灵感: Shoulda::Matchers::ActiveRecordShoulda::Matchers::ActiveModel ,以及这个StackOverflow关于结构化应该在模型中进行测试的答案 。 但是,还有一些我不确定的事情,我想知道如何使这些测试更好。

我将使用Rails教程中的用户规范作为我的示例,因为它是最详细的,并涵盖了许多可以改进的领域。 以下代码示例已从原始user_spec.rb更改,并将代码替换为describe "micropost associations"行。 针对user.rb模型的规范测试及其工厂在factories.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@fborg 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答案中的注释说“我只测试与行为相关的事情,我不考虑列的存在或索引行为。数据库列不会消失,除非有人故意删除它们,但是你我可以通过代码审查和信任来保护它,“我同意这一点,但是有没有任何正当理由可以测试数据库模式的结构,从而certificate了Shoulda::Matchers::ActiveRecord模块的存在? 也许只是重要的指标值得测试……?
  2. 是否should have_many"associations"下进行should have_many测试,在"model attributes"下替换相应的should respond_to测试? 我不知道是否should have_many测试只是在模型文件中查找相关的has_many声明,或者实际执行与should respond_to相同的函数。
  3. 您是否有任何其他意见/建议可以使这些测试在内容和结构上更加简洁/可读/彻底?

1)Shoulda :: Matchers :: ActiveRecord模块比只有列和索引匹配器还要多得多。 我会在附带的课程中挖掘一下,看看你能找到什么。 这就是have_manybelong_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。 当我看到你的对象响应“跟随?”这样的东西时,我可以做出假设,但不知道它意味着什么。 是否需要争论? 它返回一个布尔值吗? 对象是跟随某个东西还是跟在对象之后的东西?

您的问题涉及几点,我想解决其中两个问题:

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

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

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

我认为应该从规范的角度来看待这一切。

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

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

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

对于“我们真的需要这个”问题:它取决于很多方面:应用程序复杂性,安全性是否关键,遵循的标准,合同/法律/工业法规等。

通常我会说,对于正确的理想应用程序,负责unit testing的要求应该编写单元级规范,测试人员应该根据此规范实现测试。

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

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

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 

希望有所帮助。