In Progress
Unit 1, Lesson 1
In Progress

Where do Private Methods Come From?

A question that I hear a lot is: “Do you test private methods? And if so, how?” In this, the first of a series, we’ll talk about where private methods come from, and what that has to do with testing them (or not).

Video transcript & code

Over the years I’ve done a lot of training and coaching on unit testing and test-driven design. And there’s this question that comes up a lot:

Do you test private methods? And if so, how?

This is a question that generates a lot of debate in Ruby-land. Some programmers are adamant about only testing public interfaces. Others break privacy in order to test the logic in private methods. Still others cite the difficulty of testing as one reason for never making methods private.

Me, I use private methods, and I don’t write tests for them. But the reason I don’t test them flows from how my private methods come into existence in the first place.

To illustrate what I mean, let’s take a look at some example code.

Here are the tests for an extremely rudimentary reverse-Polish notation calculator.

We have a test for the addition functionality,

where we create a calculator instance,

prepare an expression that pushes the numbers 3 and 2 onto the stack and then applies the addition operator,

Then asserts that the result is 5.

Similarly, there’s a subtraction test

that pushes a 10 and a 7 onto the stack and then applies the subtraction operator

and asserts that the result is 3.

Let’s take a quick look at the implementation so far.

The Calc class has a stack attribute,

which it initializes to an empty array.

When it is told to calculate an expression,

It splits it into tokens on whitespace

And then switches on the contents of the token.

Numbers are parsed into integers and pushed onto the stack.

If it encounters a + operator

It pops the left and right operand off the stack,

then pushes the result of adding those two operands back onto the stack.

The subtraction case works quite similarly.

At the end, the calculate method returns the last value on the stack.

require "minitest/autorun"

class TestCalc < MiniTest::Test
  def test_add
    calc = Calc.new
    expr = "3 2 +"
    assert_equal 5, calc.calculate(expr)
  end

  def test_subtract
    calc = Calc.new
    expr = "10 7 -"
    assert_equal 3, calc.calculate(expr)
  end
end

class Calc
  attr_reader :stack

  def initialize
    @stack = []
  end

  def calculate(expression)
    expression.split.each do |token|
      case token
      when /^\d+$/
        stack.push(Integer(token))
      when "+"
        right = stack.pop
        left = stack.pop
        stack.push(left + right)
      when "-"
        right = stack.pop
        left = stack.pop
        stack.push(left - right)
      else
        fail "Unexpected token: '#{token}'"
      end
    end
    stack.last
  end
end

These tests are not what I’d call comprehensive.

But they do exercise almost all of the implementation code. And they only test public methods.

In fact, the current implementation code only has public methods.

Looking at the implementation code, we can observe that the addition and subtraction cases are almost completely identical. Before we add any new operators,

let’s refactor this code to eliminate some logic duplication.

we’ll have just once case branch for both binary operators

As before, it will pop left an right operands off the stack

but then, we use another method to look up an operator implementation.

We apply that implementation by calling it.

And we save the result to the stack.

Now let’s create that lookup_binary_operator method.

We’ll add a private section to the class

And we’ll add the method.

Inside it, we create a table mapping operators to lambdas that implement those operators.

And at the end we use fetch to look up and return the given operator.

class Calc
  attr_reader :stack

  def initialize
    @stack = []
  end

  def calculate(expression)
    expression.split.each do |token|
      case token
      when /^\d+$/
        stack.push(Integer(token))
      when "+", "-"
        right = stack.pop
        left = stack.pop
        op = lookup_binary_operator(token)
        result = op.call(left, right)
        stack.push(result)
      else
        fail "Unexpected token: '#{token}'"
      end
    end
    stack.last
  end

  private

  def lookup_binary_operator(token)
    {
      "+" => ->(left, right) { left + right },
      "-" => ->(left, right) { left - right },
    }.fetch(token)
  end
end

When we run the tests, they still pass.

Is this new private method tested? Yes! Our refactoring moved and reorganized logic that was already tested from a public method into a private one. So its behavior is tested, by definition! We can prove it by bringing in simplecov and taking a look at the code coverage report.

What we’ve done here is extract the logic for picking out an operator implementation into a new method. And that’s where almost all of my private methods come from: extraction.

Now I know what you’re thinking: this is all too easy. Sure, private methods are well-tested if they are just extracted out of tested public methods. But what about when we need to add to the logic that exists in already-extracted private methods?

Indeed, that’s where things get sticky, and we’ll talk about it in an upcoming episode. Happy hacking!

Responses