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!