具有多个复杂私有方法的单个 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.new
; servant.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,我只会做一些集成测试以确保一切都正确连接在一起。
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.new
; servant.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,我只会做一些集成测试以确保一切都正确连接在一起。