Testing Private Methods

Periodically the question of how to test private methods comes up at work or online. My answer is: don’t. It may seem trite, but there is some reasoning behind it.

Private methods are, by definition, implementation details. If you are approaching your tests from a behavioral standpoint – and you really should be, whether you are using a full “BDD” framework or not – you should not be testing implementation, only outward behavior. For this reason alone you shouldn’t be testing private methods.

However, if you are as “test-infected” as I am you prefer to write every bit of code test-first, and sometimes you need a private method with a little more than trivial complexity. Shouldn’t you isolate it and write it test-first like any other code?

I submit that if you are writing private methods from scratch, you may be doing it wrong.

I submit that private methods should be extracted as part of a refactoring, never constructed from scratch. They should be the result of pulling working code out of a public method in order to DRY up duplication or to simplify the implementation of the public method. And since refactoring is by definition something you perform on tested code, your private methods are already tested.

But what about the case of complex-yet-private code? When you have a private method which is complex enough to warrant tests of it’s own, that’s your code’s way of telling you it wants to be broken up into more classes.

An example would be good right about now. Here’s a blog post class. We want to be able to generate a “slug” version of the title – a string suitable for use as part of a URL.

class BlogPost
  attr_reader :title
  
  def title_slug
    slugify(title)
  end

  private

  def slugify(string)
    # ???
  end
end

We want to implement slugify in a test-first way, giving it several different example strings and verifying it produces the expected slug. Isn’t this a case for testing a private method?

What if, instead, we broke it out into it’s own nested class?

class BlogPost
  attr_reader :title
  
  def title_slug
    SlugString.new(title)
  end

  class SlugString < String
    def initialize(string)
      # ???
    end
  end
end

Now we can test BlogPost::SlugString to our hearts content. We haven’t resorted to ugly hacks to get around the method privacy protection. Our concerns are better separated now: BlogPost is only concerned with representing blog posts, not with text munging. And we get another benefit as well: we can easily mock out SlugString in our tests for BlogPost. By listening to the code when it asked for a separate class, we’ve stumbled on a better-factored design.

8 comments

  1. Couldn't agree more. I often hear questions about testing private methods and to me it makes no sense whatsoever. As you say: private methods are an implementation detail!

  2. Or the slugify method could be turned into a function, since it isn't really relevant to the BlogPost instance.

    1. Ruby doesn't really support the notion of free functions (it kind of fakes it, but in a way I prefer not to take advantage of in anything but quick one-off scripts). It could be a class-level method, I suppose. I still prefer the class approach, because then if you decide to mock the slugification out you're not mocking methods on the class under test.

  3. Remember that the purpose of testing is to find bugs. There is no question of right or wrong but a question of whether testers are finding mistakes, deviations, and errors more effectively. There are many works covering the topic of software testing. I recommend the works of Cem Kaner.

    1. I believe you are confusing two two job descriptions. Some organizations have professional testers whose job, indeed, is to find bugs. Typically they are not the ones writing unit tests. They are usually writing much higher-level full-system test scripts. When I, as a developer, write unit tests I'm doing it to drive out a good design. The fact that the resulting test suite is good at catching regressions is a handy side-effect.

Leave a Reply

Your email address will not be published. Required fields are marked *