Test Objects, Not Methods

Posted July 16, 2009 by Industrial Logic

Black BoxI'm so often asked: "When microtesting, doing TDD, is it okay to test a private method?" The answer, at least literally speaking, is no! Let me explain. Imagine I begin test-driving a new object. As I consider my first test, I have to answer a design question: "What does this object do?" In other words, what can other objects ask of it and what should they expect to get or happen? I decide, then merrily go about writing tests to tease out the interesting details of this general behavior. Before long, I find something I need this object to do that complicates things. Complicates them to a point that tempts me to ponder, "Gee, I could just expose that new tricky private method by making it protected and test its logic directly. That sure would make things cleaner and simpler." And in one sense, I'd be right - that code deserves its own tests. But in a more important sense, I'd be wrong. Wrong in that what I should have realized is that this private method on Object A is really just begging to be the public method on a new Object B.
This private method really just wants to be a public method on another new object! Boom, I've just made a design decision! And wow, one that was driven out of just listening to my tests.
That method was private on Object A because it wasn't part of its "observable behavior." It wasn't "what the object does," it was an implementation detail of "how it does it." As soon as I started testing it directly, I had broken the fundamental rule of testing only the observable behavior of objects. I had started "testing methods" and, worse, a private one at that! Instead, I can and should remain true to the rule by making that logic the observable behavior of another object, one that my top-level object will then delegate to. Under the covers, interestingly, what's really happened is that I've paid homage to the Single Responsibility Principle. Upon making the decision about what that original object "does," I had established its responsibility, and embarked on encoding that into its tests. Once I wanted tests outside the scope of that established responsibility, I had found a new object, a new abstraction, asking for that behavior as its responsibility. This is primarily why the idea of microtesting being "white-box" testing so bothers me; microtesting is still "black-box" testing, and the box you shouldn't be looking inside of is the object. So, coming full circle, hopefully you'll see and agree how this boils down to a simple but powerful rule of thumb:
Keep yourself focused on writing tests for the desired behavior of your objects, rather than for the methods they contain.
When you can do this, you're sure to get more of TDD's real benefit - its power as a design tool!