Test Smells

Flashcard 5 of 5

Should private methods be tested?

No.

Well...yes, but not directly.

Here's what you don't want to do:

describe "#generate_widgets" do
  it "generates 5 widgets (which are private)" do
    user = create(:user)

    user.generate_widgets
    count =
      user.send(:private_method_returning_widget_count)

    expect(count).to eq 5
  end
end

See that send there? We recommend you stay away from that.

Testing private methods directly is undesirable because they should be an implementation detail hidden from outside callers. Using send effectively unhides them.

Private methods should support your object's public methods. Since those public methods are tested, the private methods are already tested implicitly.

Now this doesn't mean we don't want to have test coverage for private methods, it just means that we need to test it through the public API of our object. Any behavior your object has should be covered and documented by your tests.

Sometimes you'll find that a lot of complexity has crept into a private method, and testing it through the public interface is difficult. In this case, consider extracting a new class where that private method can be public.

Here's a quick example:

class User
  def save
    # do some stuff
    sanitize_username
    # do more stuff
  end

  private

  def sanitize_username
    self.username = username.chomp
  end
end

Right now, sanitize_username is quite simple, and we can test it by saving a user with some extra whitespace on their username.

Imagine we later decide to strip out profanity as well. Clearly, sanitize_username is going to grow in complexity, and we might need many (hilarious) test-cases. Each of these tests will be testing through the save method. Wouldn't it be nice if we could simply call sanitize_username directly?

It sure would! But we're not going to reach for send.

Instead, we could do the following:

class User
  def save
    # do some stuff
    sanitize_username
    # do more stuff
  end

  private

  def sanitize_username
    self.username = UsernameSanitizer.new(username).sanitized
  end
end

class UsernameSanitizer
  def initialize(username)
    @username = username
  end

  def sanitized
    # Relevant sanitizing code lives here now.
  end
end

Notice that sanitized is now public, and we can test it directly. No more profane user names, and simple, direct tests. Life is good!

Return to Flashcard Results