Test your private methods
How do you test private methods in Ruby? Well, you could use send, or import the class into the testing namespace, or make them public. Or you could do none of these, because the overwhelming consensus in the testing community is that Testing Private Methods Is Bad and You’re Doing Something Wrong if you try. Some tests I wrote broke that convention and sparked a 20 minute discussion among all of the engineers in my office, which eventually resolved to “if you feel so strongly about this, write an essay.” I think the assumption was that I’d be mature and professional enough to let it drop and not actually spend a bunch of hours writing an essay.
Never overestimate my maturity.
Generally, there are four main arguments why you shouldn’t test private methods.
- Test the behavior, not the implementation
- Testing private methods are brittle
- Testing private methods exposes the inner methods of a class
- Testing private methods is a code smell.
Let’s tackle them one by one.
Joining the engineering cool kids
“Test the behavior, not the implementation.”
If you want to find strong sources that disagree with me, just google that phrase. Everybody says it. Public methods are the behavior of your class and the private methods are the implementation. If you test private methods, you’re testing “how” the class does its stuff, which is strictly inferior to just testing the “what”. The reasons make a lot of sense when you read them:
- If you change implementation but maintain correct behavior, implementation tests might break, leading to brittleness and refactoring.
- If you change implementation and get incorrect behavior, behavior tests will break anyway.
Or, because I’m obsessed with charts:
Will the tests fail?
bad fail fail
good fail pass
So far, so good. There’s a clear advantage of behavior tests over implementation tests: they’re stabler and don’t give you false failures. Also long as the class is visibly doing what its supposed to do, we don’t need to know the bizarre logic it used to get there. No surprise “Test behavior, not the implementation” is the convention in software engineering.
Interestingly enough, though, this makes software engineering the exception. In other forms of engineering it’s common practice to test the implementation of a system, not just its behavior. I used to work in an astrophysics lab and part of testing our sensors involved carefully measuring its internal resistance. There’s a lot of quite interesting research and testing done on screws and weld verification is hugely important to mechanical engineering. Even non-engineering fields test both behavior and implementation: raise your hand if you’ve ever been dinged on a math test for not showing your work.
This is because comprehensively testing behavior is really freaking hard. Testing small bits of implementation, combined with testing the overall behavior, tells you much more about the system than testing the behavior alone. This gets even more important when you’re adding things besides testing comprehensiveness into the mix, such as time to run and overall code quality. Let’s take the following oversimplified dummy class:
array = do_something_really_expensive
# blah blah blah
How do we test this? If we want to keep to “test behavior not implementation”, there’s a few ways this can go:
- Write a couple of behavior tests, say “looks about right”, get slapped by the bug
- Write a dozen behavior tests that cover all of the possible edge cases, watch the test time skyrocket from all the calls to do_something_really_expensive
- Stub out do_something_really_expensive, realize this implicitly tests farble_time’s implementation, curse God
- Make the sort a public method so you can test it, realize this exposes implementation as behavior, shake your fist at the heavens
- Replace with ArraySorter.new(array).sort, extensively test the behavior of sort, realize you’ve added a bunch of code complexity and an extraneous NounVerber class that still exposes implementation as behavior, weep
On the other hand, if we bite the bullet and test the implementation in addition to the behavior, it’d look something like this:
- Write a bunch of tests on subtly_bugged_sort, discover and crush the bugs
- Write a couple of behavior tests
Tests as Scaffolding
“But wait”, you could say, “That misses the point! If you change the implementation, then you have to change the tests too!”
“That makes them brittle and useless!”
I think that position misses a lot of the power of testing. Tests aren’t just there to ensure your contracts are still the same. They’re also there to ensure that business requirements are being followed, the UI looks good, your server can handle stress, new bugs don’t enter the code, etc. In addition, they help shape your development and provide a rigorous scaffold around your code. That’s why we do ‘red-green-refactor’ cycles. Heck, that’s why we have unit tests in the first place. Otherwise we could just get away with acceptance tests.
If you’re building a new implementation, you need a new scaffold. Tear down your obsolete implementation tests and build new ones. Ideally your implementation tests should be small and contained enough that this isn’t a problem. If it is, then either your implementation needs work or you’re also testing behavior in your implementation tests and you need to write more behavior tests.
Behavior tests should be strong and stable. But there’s no similar requirement on implementation, and dinging them for not being behavior tests misses the unique contribution they give your suite.
“Regardless of the practicality, you’re still exposing a private method, which is bad.”
Why do we make methods private? Let’s do some quick background. Ruby was heavily influenced by Smalltalk, which was the first real object-oriented besides Simula, Algol, and a bunch of others I’m ignoring for propaganda reasons. In Smalltalk, the idea of “private” only really extended to instance variables. You could make methods private, but that’d be true only in the documentation. Obviously, there’s a lot of benefit to having private variables. The typical example here is bank software. You don’t want to let anybody just say “Account.balance += 1”. Instead they have to use deposit and withdraw methods that can enforce validations. Private methods, though, aren’t mutable and don’t have any invariants we need to enforce, so there’s really no need for them.
Ruby, on the other hand, does have Actual For Real Private Methods. That’s because while they might not provide any functionality, they provide a hell of a lot of usability. With them we can DRY up our public methods without letting others use our object in unexpected ways. This is where the behavior/implementation split comes from. Public methods expose the functionalities the rest of your program can use, while private methods keep the housekeeping out of the object’s API. That’s why it’s a problem when you expose your private methods: if you need to use the method, why is it private in the first place?
But your tests, though, are not your program, in the same way that your syntax checker and your profiler aren’t your program. They’re all tools for telling you things about your program; in the case of your test suite, how buggy it is. If the private methods provided something you couldn’t get without them, there’d be a good argument to not testing them. That’s similar to how it’s a bad idea to dig around in the instance variables, as you’re likely to break an invariant. But they exist to make it easier to organize classes. Convenience shouldn’t come at the cost of reduced test integrity.
When you expose private methods to your program, you’re reducing the maintainability of your code. When you expose private methods to your test suite, you’re giving it access to a deeper layer of the code than the public methods can provide. It’s fine to ask your profiler whether a specific part of your implementation is slow, so why should it be any different to ask whether a specific part of your implementation is buggy?
Spoopy Testing at a Distance
“Testing private methods is still a code smell.”
While rubber ducking this essay off a friend he brought up an interesting point: “One class’s implementation is another class’s behavior.” This kind of sentiment is common in the TDD and BDD worlds. If you need a test a private method, that probably means your class is too complex and you should probably factor the logic out into a new class. Essentially, this is another form of the SRP: classes should do one thing and one thing only.
What is “one thing”, though? ArrayQuicksorter? ArrayQuicksortPivotFinder? ArrayQuicksortPivotElementComparer? SRP is important, but it’s important in the context of your larger program. By making something a class, you’re declaring it a reusable component in your program. Additionally, you’re declaring it as publicly available. Pulling ArraySorter out of TimeFarbler might mean you don’t have to test a private method, but it also means that now other classes might call ArraySorter. That increases the coupling in your program and makes it harder to reason about your code. If you change a private method, you only have to guarantee that the public methods are still valid. But if you change a public method, you have to guarantee that all consumers of the object are still valid. And when you follow SRP too strictly you’re going to have a lot more public methods.
Of course, SRP is still a great rule of thumb and often you should be considering whether to break out your private methods. But just because it smells doesn’t mean it’s rotten.
I’m not saying that more private methods is always good, nor that more classes are always bad, nor that we always want to be testing implementations. What I am trying to say is that testing implementation has value in contexts, and that applying a rote rule of “never test your implementation” is, in the long run, harmful. Programming is as much an art as a science, and following practices as rules as opposed to guidelines takes away your ability to choose the best possible option in a given context. Sometimes it makes sense to have classes with complex implementations, and in those cases it makes sense to validate those implementations. So don’t feel bad about testing your private methods. You know your code better than any book or blog post could.