【问题标题】:Should I write these Test Cases?我应该写这些测试用例吗?
【发布时间】:2017-06-23 08:42:07
【问题描述】:

这是我要测试的模型的架构:

create_table "retreats", force: :cascade do |t|
  t.string   "title"
  t.string   "tagline"
  t.string   "type_of"
  t.datetime "created_at",                 null: false
  t.datetime "updated_at",                 null: false
  t.string   "description"
  t.string   "schedule"
  t.boolean  "available",   default: true
end

这是撤退模型:

class Retreat < ApplicationRecord
  TYPES_OF_RETREATS = ['individual', 'group']
  validates :title, presence: true
  validates :type_of, presence: true, inclusion: {in: TYPES_OF_RETREATS,
    message: "%{value} is not a valid type."}

  has_many :testimonials, dependent: :destroy
  has_many :images, dependent: :destroy
  has_and_belongs_to_many :dates, class_name: "RetreatDate", foreign_key: 
   'retreat_id', association_foreign_key: 'retreat_date_id'
end

这些是我写的测试用例:

  test "retreat should not save without a title" do
    retreat = retreats(:no_title)
    assert_not retreat.save, "Saved a retreat without a title"
  end

  test "retreat should not save without a type" do
    retreat = retreats(:no_type)
    assert_not retreat.save, "Saved a retreat without a type"
  end

  test "retreat can have a tagline, description, schedule and available" do
    retreat = retreats(:all_attributes)
    assert retreat.save, "Retreat failed to save"
  end

  test "retreat type should be from the provided list" do
    retreat = retreats(:invalid_type)
    assert_not retreat.save, "Some other retreat got saved. It shouldn't 
     have gotten saved."
  end

  test "retreat can have many testimonials" do
    retreat = retreats(:one)
    retreat.testimonials << Testimonial.new(statement: 'this is my 
      testimonial', participant_name: 'abc')
    assert retreat.save, "Retreat did not save with the testimonials."
  end

  test "retreat can have many dates" do
    retreat = retreats(:one)
    retreat.dates.create({date: '02-08-2012'})
    retreat.dates.create({date: '02-08-2013'})
    assert retreat.save, "Retreat with multiple dates is not saving"
    assert_equal(2, retreat.dates.length, "Retreat isn't saving multiple 
       dates.")
  end

我正在寻找有关我应该为哪种测试用例编写测试的建议。我觉得我的一些测试用例是不必要的。像用于验证的测试用例是有意义的,但测试我是否可以添加多个推荐让我感到不舒服。

我可以像这样重写前两个测试用例:

test "retreat title and type_of must not be empty" do
  retreat = Retreat.new
  assert retreat.invalid?
  assert retreat.errors[:title].any?, "Title must exist"
  assert retreat.errors[:type_of].any?, "Type must exist"
end

编写单元测试的最佳实践是什么?以及如何编写更好的单元测试?

谢谢:)

【问题讨论】:

    标签: ruby-on-rails unit-testing testing


    【解决方案1】:

    http://www.betterspecs.org/ 在开始测试方面帮助了我很多。

    我会说坚持测试您的业务规则。所以不要只测试保存或关系,因为它们有效并且是 Rails 框架的一部分。但是做测试规则比如:'它可以有 2 个日期','我希望能够同时创建一个撤退和推荐'

    【讨论】:

    • 业务规则类似于“一次撤退可以有很多推荐”?我浏览了链接,但不是 rspec 的链接吗?我一直在寻找特定于 minitest 框架的东西,这是 Rails 中的默认框架。
    • 该链接确实适用于 Rspec,但它显示了可应用于任何测试框架的最佳实践。业务规则确实是这样的:A retreat can have many testimonialsA retreat must have at least one testimonial
    【解决方案2】:

    我认为您需要在“外部”和“内部”之间建立更清晰的界限。 Rails 本身(或在这方面实际的 ActiveRecord)在这里没有帮助。它污染了你的对象,有很多责任不清楚它们属于哪里:ActiveRecord 不是测试的最佳接口。

    我遵循单元测试规则

    仅测试自己的(公共)界面及其对直接合作者的影响。

    write unit tests that test only the subject under test (internal) and never more 是常见的做法。任何与被测对象、单元协作的东西都是外部的:它们有自己独立的单元测试。

    这会导致大量的嘲笑和存根。一个典型的非 AR 示例是:

    class BikeShed
      attr_accessor :color, :colorizer
      def initialize(color, colorizer = ColorFactory)
        @color = color
      end
    
      def discuss
        @color = ColorFactory.random
      end
    end
    

    测试看起来像:

    class BikeShedTest < TestCase
      describe "#discuss" do
        it "changes color to a random color from ColorFactory" do
          subject.color_factory = stub("DefaultColor")
          subject.color_factory = color_factory
          color_factory.expects(:random).returns(color)
    
          subject.discuss
    
          assert_equal color, subject.color
        end
      end
    
      private
    
      def subject
        @subject ||= BikeShed.new
      end
    
      def color
        @color ||= stub("Color")
      end
    
      def color_factory
        @color_factory ||= stub("ColorFactory")
      end
    end
    

    我使用依赖注入来传递它的所有协作者,并且只测试主题 是否以正确的方式与这个关联进行交互。而已。永远。

    我使用了BikeShed,因为这是一个备受争议的话题;如果没有适当的集成测试,这种测试风格很糟糕,它可能导致您只测试您是否正确设置了存根。它还可以相当快地导致“测试实施”。 然而,我真的很喜欢这种风格,因为它迫使您保持松散耦合并保持 API 和职责小、专注和干净。诸如此类的东西ActiveRecord 中断。

    ActiveRecord 污染了具有大量责任的模型。验证、存储、回调、编组、映射到视图文件、has-many 等、范围、缓存等等。

    因此,w.r.t. ActiveRecord(和大多数 Rails 对象),我遵循:

    Rails 在超类中提供的一切都是协作者

    我认为ActiveRecord::Base 好像它是一些外部 API。就像上面例子中的ColorFactory。尽管这在技术上并不真正正确:它也是我的 API 的一部分。考虑一下:如果您有一个继承的Stripe::Payment,比如在您的MonthlyPayment 中,您将无法测试Stripe 是否正确地从您的CC 获取资金,甚至Strip 在其服务器上创建了正确的付款。那么为什么这与您的数据库不同呢? ActiveRecord 只是通往数据库的网关,就像 Stripe::Payment 一样。

    所以,在测试 ActiveRecord 时,我会考虑 ActiveRecord 提供的任何东西,作为外部 API,我将其模拟出来:

    class ApplicationRecord < ActiveRecord::Base; end
    class Retreat < ApplicationRecord
      validates :title, presence: true
      scope :nsfw -> { where("title LIKE '%nsfw%'") }
    end
    

    测试可能看起来像

    class RetreatTest < TestCase
      describe ".nsfw" do
        it "selects only records whose title includes nsfw" do
          ActiveRecord::Base.expects(:where).with("title LIKE '%nsfw%'")
          subject.nsfw
        end
      end
    
      describe "#title" do
        it "is validated to be present" do
          subject.title = nil
          subject.validate
          assert_includes subject.errors["title"], "can't be blank"
        end
      end
    
      private
    
      def subject
        @subject ||= Retreat.new
      end
    end
    

    我们在这里看到了三件重要的事情:

    1. 测试作用域只是为了确定我们正在使用正确的消息和参数调用 external ActiveRecord API。我们可以放心地假设 ActiveRecord 有适当的测试来断言当我们正确调用它时,它将从某些存储中返回正确的属性。那不是我们的责任。 (但集成测试应该断言用户的最终结果是经过适当过滤的视图等)。
    2. 测试像validate 这样的 只是测试我们的模型配置是否正确;不幸的是,确定此配置的 API 非常糟糕,因此我们确实运行了验证以查看它在我们的主题上设置了某个错误。
    3. 唯一性验证更难,我们需要存根 ActiveRecord.any? 以返回 true,然后期待验证 .error 出现。例如。但是我们永远不应该将记录写入数据库,然后使用该设置来确定我们对象上的验证错误:这是脆弱的,我们正在测试很多完全不相关的东西。例如它存储正确(AR 的责任,不是我们的模型)或者 AR 使用正确的where 来确定唯一性(AR/ORM 的责任,而不是我们的模型)。

    请注意,这听起来像是“测试实现,但实际上并非如此:我们正在测试以某种方式调用外部 API。这是 IMO 单元测试的一项(也是唯一)任务。集成测试是在那里断言所有这些“某些方式”都会导致正确的行为。

    【讨论】:

    • “内部”是指 Rails 框架吗?我的“外部”是指我编写的代码吗?有很多我不理解的术语,例如嘲笑和存根。有适合我这种初学者的资源吗?
    • 我已更新答案以涵盖您的一些问题。在martinfowler.com/bliki/TestDouble.html 可以找到一些基本术语。但请注意,一些测试框架(rspec,你,愚蠢的白痴!)使用的术语完全相反。
    猜你喜欢
    • 2019-03-24
    • 2011-06-26
    • 2010-12-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多