具有多种复杂私有方法的单一公共方法的TDD流程

时间:2016-09-09 12:55:51

标签: unit-testing language-agnostic tdd

  

注意:问题 "should I test private methods or only public ones?" 非常适合我提出的问题。

我的问题是:使用复杂的私有方法构建单一,防弹的可靠公共方法,最实用的TDD 流程是什么?

我最好通过例子来学习,所以这里有:

第1章)测试覆盖率

说我有一个只做一件事的红宝石课,它给了我培根。

它可能看起来像这样:

class Servant
  def gimme_bacon
    # a bunch of complicated private methods go here
  end

  private

  # all of the private methods required to make the bacon
end  

现在我可以致电servant = Servant.new; servant.gimme_bacon。太棒了,这就是我所关心的。我想要的只是我的培根。

但是说我的仆人很糟糕。这是因为他还没有任何私人方法,所以gimme_bacon只返回nil。好吧,没问题,我是开发人员,我会给Servant类所有正确的私有方法,所以他最终可以gimme_bacon

在追求可靠的仆人时,我想要TDD他所有的方法。但等等,我关心的是他要去gimme_bacon。只要我在一天结束时拿到培根,我真的不在乎他必须采取的所有步骤。毕竟,gimme_bacon是唯一的公共方法。

所以,我写这样的测试:

RSpec.describe Servant do
  let(:servant) { Servant.new }

  it "should give me bacon when I tell it to!" do
    expect(servant.gimme_bacon).to_not be_nil
  end
end

尼斯。我只测试了公共方法。完美,100%的测试覆盖率。我继续进一步发展gimme_bacon能力,并完全相信它正在接受测试。

第2章)编写moar私有方法

经过一些开发(不幸的是,不是TDD,因为我添加了私有方法)我可能会有这样的东西(伪代码):

class Servant
  attr_reader :bacon

  def initialize(whats_in_the_fridge)
    @bacon = whats_in_the_fridge[:bacon]
  end

  def gimme_bacon(specifications)
    write_down_specifications(specifications)
    google_awesome_recipes
    go_grocery_shopping if bacon.nil?
    cook_bacon
    serve
  end

  private

  attr_reader :specifications, :grocery_list

  def write_down_specifications(specifications)
    @specifications = specifications
  end

  def google_awesome_recipes
    specifications.each do |x|
      search_result = google_it(x)
      add_to_grocery_list if looks_yummy?(search_result)
    end
  end

  def google_it(item)
    HTTParty.get "http://google.com/#q=#{item}"
  end

  def looks_yummy?(search_result)
    search_result.match(/yummy/)
  end

  def add_to_grocery_list
    @grocery_list ||= []
    search_result.each do |tasty_item|
      @grocery_list << tasty_item
    end
  end

  def go_grocery_shopping
    grocery_list.each { |item| buy_item(item) }
  end

  def buy_item
    1_000_000 - item.cost
  end

  def cook_bacon
    puts "#{bacon} slices #{bacon_size_in_inches} inch thick on skillet"
    bacon.cooked = true
  end

  def bacon_size_in_inches
    case specifications
    when "chunky" then 2
    when "kinda chunky" then 1
    when "tiny" then 0.1
    else
      raise "wtf"
    end
  end

  def serve
    bacon + plate
  end

  def plate
    "---"
  end
end

结论:

事后看来,这是很多私人方法。

可能有多个失败点,因为我没有真正的TDD任何一个。以上是一个简单的例子,但如果仆人必须做出决定,比如根据我的规格去哪家杂货店怎么办?如果互联网出现故障并且无法进行谷歌等等该怎么办?

是的,你可以说我应该做一个子类,但我不太确定。我想要的只是一个有一个公共方法的类。

为了将来参考,我在TDD过程中可以做得更好吗?

2 个答案:

答案 0 :(得分:2)

我不确定你为什么这么认为,因为他们是私人的方法,他们不能成为TDD&#39; d。它们是私有方法(或50个不同类)的事实是测试培根仆人所需行为的实现细节。

为了完成私有方法中的所有内容,你的类必须具有

  • 依赖关系
  • 输入

否则它会像第一个例子中那样返回一些培根。

这些输入和依赖关系是在TDD时驱逐测试的关键,即使这些输入导致私有方法。您仍然只会通过公共接口进行测试

所以在你的第二个例子中,你有一些你在gimme_bacon方法中传递给你的类的规范(ruby不是我的东西,所以请原谅任何误解)。您的测试可能如下所示:

When I ask for chunky bacon I should get bacon that's 2" thick
When I ask for kinda chunky bacon I should get bacon that's 1" thick
When I ask for tiny bacon I should get bacon thats 0.1" thick
When I ask for an unsupported bacon chunkyness I should get an error telling me 'wtf'

您可以在添加定义培根提供商所需行为的测试时逐步实现此功能

当你不得不去google外部时,你就会有依赖的互动。你的类应该允许切换这些依赖项(我相信在ruby中很简单),这样你就可以轻松地测试在类边界发生的事情。所以在你的例子中你可能有一个食谱查找器。你将它传递给你的班级,并在你的测试中给出它

  • 找到食谱的人
  • 一个找不到任何
  • 的人
  • 一个错误

每次你编写一个测试,说明当你的依赖行为以某种方式运行时你期望你的类的行为。然后创建一个以这种方式运行的依赖项,并在您的类中实现所需的行为。

所有TDD&#39; d,无论这些方法是否属于私有。

答案 1 :(得分:1)

当一个类变得非常复杂时,可能是时候通过将片段委托给一些从属类来分解它。想想单一责任原则。主要课程负责协调培根过程,有一个类来查找食谱等。每个下属类可以通过公共方法进行TDD,其中包括其行为的所有不同变体。对于主类我只做一些集成测试,以确保所有内容都正确连接在一起。