具有多个复杂私有方法的单个 public 方法的 TDD 过程

TDD process for single public method with multiple complex private methods

Note: the question "should I test private methods or only public ones?" is a great reference to what I'm asking.

我的问题是:什么是最实用的 TDD 过程,用于构建具有复杂私有方法的单一、防弹可靠的 public 方法?

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


第 1 章) 测试范围

说我有一个 ruby class 只做一件事,它给我培根。

它可能看起来像这样:

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.newservant.gimme_bacon。太棒了,这就是我所关心的。我只想要我的培根。

但是说我的仆人有点烂。那是因为他还没有任何私有方法,所以 gimme_bacon 只是 returns nil。好吧,没问题,我是开发者,我会给 Servant class 所有正确的私有方法,让他最终可以 gimme_bacon.

在我追求可靠的仆人的过程中,我想对他的所有方法进行 TDD。但是等等,我只关心他会 gimme_bacon。我真的不在乎他必须采取的所有步骤,只要我在一天结束时得到培根即可。毕竟,gimme_bacon是唯一的public方法。

所以,我这样写我的测试:

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

不错。我只测试了 public 方法。完美,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

结论:

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

可能有多个失败点,因为我并没有真正对其中任何一个进行测试驱动开发。上面是一个简单的例子,但是如果仆人必须做出决定,比如根据我的要求去哪家杂货店呢?如果互联网出现故障,他无法 google,等等

怎么办?

是的,你可以说我或许应该做一个子class,但我不太确定。我只想要一个 class 有一个 public 方法。

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

我不确定您为什么会这样想,因为它们是私有方法,不能进行 TDD。它们是私有方法(或 50 种不同的 classes)这一事实是测试培根仆人所需行为的实现细节。

为了在私有方法中完成所有操作,您的 class 必须具有

  • 依赖关系
  • 输入

否则它只会 return 一些培根,就像第一个例子中那样。

这些输入和依赖项是在您进行 TDD 时推动测试的关键,即使这些输入会导致私有方法。您仍然只能通过 public 接口进行测试

因此,在您的第二个示例中,您有一些规范要在 gimme_bacon 方法中传递给您的 class(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 东西时,你就会与依赖项进行交互。您的 class 应该允许切换这些依赖项(我认为这在 ruby 中很简单),这样您就可以轻松测试 class 边界处发生的情况。所以在你的例子中你可能有一个食谱查找器。你将它传递给你的 class 并在你的测试中将它传递给它

  • 找到食谱的人
  • 没有找到
  • 一个错误
  • 等等
  • 等等

每次你写一个测试,说明你期望你的 class 的行为是什么,当它的依赖以某种方式表现时。然后,您创建一个以这种方式运行的依赖项,并在 class.

中实现所需的行为

所有 TDD,无论这些方法是否私有。

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