vendredi 9 septembre 2016

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.

My question is: what is the most practical TDD process for building out a single, bulletproof reliable public method with complex private methods?

I best learn through examples, so here goes:


Chapter 1) Test coverage

Say I have a ruby class that only does one thing, it gives me bacon.

It would probably look something like this:

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  

Now I can call servant = Servant.new; servant.gimme_bacon. Awesome, that's all I care about. All I want is my bacon.

But say my servant kind of sucks. That's because he doesn't have any of his private methods yet so gimme_bacon just returns nil. Alright, no problem, I'm a developer, I will give the Servant class all of the right private methods so he can finally gimme_bacon.

In my pursuit for a reliable servant, I want to TDD all of his methods. But wait, all I care about is that he is going to gimme_bacon. I really don't care about all of the steps he has to take as long as I get my bacon at the end of the day. After all, gimme_bacon is the only public method.

So, I write my test like this:

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

Nice. I only tested the public method. Perfect, 100% test coverage. I move on to further develop the gimme_bacon capability with full confidence that it is being tested.


Chapter 2) Writing moar private methods

After some development (unfortunately, not TDD because I'm adding private methods) I might have something like this (in pseudo code):

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://ift.tt/2ciYAOW}"
  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


Conclusion:

In hindsight, that's a lot of private methods.

There could be multiple points of failure because I didn't really TDD any of them. The above is a simple example, but what if the servant had to make decisions, say on which grocery store to go to depending on my specifications? What if the internet was down and he couldn't google, etc.

Yes, you could say that I should perhaps make a subclass, but I'm not so sure. All I want is one class that has one public method.

For future reference, what could I have done better?

Aucun commentaire:

Enregistrer un commentaire