单个公共方法与多个复杂私有方法的 TDD 流程

TDD process for single public method with multiple complex private methods

提问人:binarymason 提问时间:9/9/2016 最后编辑:binarymason 更新时间:9/10/2016 访问量:150

问:

注意:“我应该测试私有方法还是只测试公共方法?”这个问题很好地参考了我所问的问题。

我的问题是:用复杂的私有方法构建一个单一的、无懈可击的可靠公共方法,最实用的TDD过程是什么?

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


第 1 章)测试覆盖率

假设我有一个只做一件事的 ruby 类,它给了我培根。

它可能看起来像这样:

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

但是说我的仆人有点烂。那是因为他还没有任何私人方法,所以只是返回.好吧,没问题,我是一名开发人员,我将为 Servant 类提供所有正确的私有方法,以便他最终可以.gimme_baconnilgimme_bacon

在我追求一个可靠的仆人的过程中,我想TDD他的所有方法。但是等等,我只关心他会.我真的不在乎他必须采取的所有步骤,只要我在一天结束时拿到培根。毕竟,是唯一的公共方法。gimme_bacongimme_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 流程中可以做得更好吗?

单元测试 与语言无关 TDD

评论


答:

2赞 Sam Holder 9/9/2016 #1

我不确定你为什么认为因为它们是私有方法,所以它们不能被TDD'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'

您可以在添加测试时逐步实现此功能,这些测试定义了培根提供程序的所需行为

当你不得不去谷歌的东西外部时,你就会与依赖项进行交互。你的类应该允许切换这些依赖项(我相信这在 ruby 中很简单),这样你就可以很容易地测试在类的边界上发生了什么。因此,在您的示例中,您可能有一个配方查找器。你把它传给你的班级,在你的测试中你给它

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

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

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

评论

0赞 binarymason 9/11/2016
我真的很喜欢你把所有东西都归结为依赖项和输入的方式。关于TDD,我想从红色到绿色的时间更长。如果我开始对 Servant 类进行编程,我可以对培根以及它是否符合规范提出这些期望。从编写该测试到实际通过实现之间的时间将相当长(与公共方法的快速响应周期相反)。我理解正确吗?
0赞 binarymason 9/11/2016
假设我有一个实例,其中 Servant 不是外部依赖项,而是通过私有方法根据输入执行了一堆数据操作和计算。我会先写一个简单的测试,但我可能需要几分钟才能通过实现,
0赞 Sam Holder 9/11/2016
在通过实现之前需要多长时间取决于实现的复杂程度。但是你倾向于一次做一个测试,然后逐步添加到实现中,所以通常时间不长。通常,私有方法只在重构阶段出现。在实施(绿色)阶段,你更专注于让它工作,而不是最终的样子
1赞 Mike Stockdale 9/10/2016 #2

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

评论

0赞 Sam Holder 9/11/2016
虽然一般来说,这是合理的,但要谨慎地单独测试你所突破的类。这样做太多会给您带来非常脆弱的测试。单元不一定是单个类,但可以是多个类,这些类协同工作以提供某些功能。我对此的一般经验法则是,如果我打算在另一个地方重用它,只为一个被重构到一个单独类中的部分编写特定的测试。如果仅此当前功能需要它,那么我将保留当前功能位的测试来测试它。YMMV。