When I can’t test it where it is, I look to move it somewhere else, where I can test it. Today’s notion isn’t so much a single refactoring as it is a strategy that can be achieved in different ways (and different multiple steps).
A modern and frequently occurring case: using a cool framework to expose service endpoints, we write a function and then we annotate it and poof, it’s an endpoint. This is how Java+Swing works, or Python+Flask.
When we give that function a body, it will be a body of "our branching logic" (OBL). We’ve already discussed that OBL is the main target of our tests, so we definitely want to test that function. Sadly, testing that usually involves one or more of three owwies.
We’ll have to either/or/and 1) fire up the app, 2) fire up the framework’s test rig, 3) instantiate the class containing the method. Most of the time, none of these three choices is a fountain of geek joy.
The problem, in all three choices, is in the weight of that framework. And it’s not a problem that’s avoidable: we chose that framework precisely cuz it does so much for us. Of course it’s heavy.
Now, be clear, this objection is not about it being "impossible" to do one of those three things. It’s not. All three of them are doable. We’re all geeks with mad skillz, and so were the framework writers. It’s doable. It’s just not wonderful.
And here’s where my recurring theme pops up: I am the boss. The code works for me. I don’t work for the code. If the code "forces" me to do one of those three unwonderful things, it’s the code that’s gonna change, not me.
So what’s the answer? Conceptually, it’s easy. (We’ll deal with some in-practice complications in a second.) It’s owwie to test the OBL code where it is, so let’s put it somewhere else, call it from where it is, then we can test it in its new easier-to-test place.
The standard "someplace else" is just another class. We start with class MyFrameworkEndpoint and method doSomething() all annotated up, with a body of OBL. We end with two classes, MyFrameworkEndpoint and MyFrameworklessHandler, and the OBL is in the new class.
Notice that word "Frameworkless". That’s important. What we’re really doing is using two classes, one that uses framework-isms, and one that doesn’t. The framework is what was forcing one of those three choices. It doesn’t help us if the new class also uses framework-isms.
One way to understand MyFrameworkEndpoint is to understand it as a boundary, part of the rim. Everything outside the rim is framework-y. Everything inside the rim is framework-less. MFE’s single responsibility is to make that translation.
Okay, the complications. Very often, the naive endpoint function freely intermixes framework code (FW) with our branching logic (OBL), something like this:
FW(); FW(); OBL(); FW(); OBL(); OBL(FW); FW(OBL);
The first trick: just re-order the code if you can. Put all the OBL code together, usually either at the end or in the middle. It’s very often just that easy. The only reason it was intermixed was because we typed it in that order, not because the ordering was required.
Sometimes, OBL has to actually have an effect on the FW. At its simplest, we handle this by having the OBL return something the FW can use to have that effect.
If it’s tricksier than that, we can use one of our dependency-inversion techniques, like observer, or callbacks, or lambdas, to make it happen. This happens every once in a while, but not so often as the naive endpoint code would make it seem.
There’s one more complication we sometimes encounter: the FW has lots of clever gimcrackery to insert sneaky FW code into other classes, and we have to not write our code that way. (@autowired is often used this way.) The answer here is simple: "don’t do that".
Anything I can cleverly inject into my frameworkless class I can also explicitly pass. I can even translate it to a frameworkless interface before I pass it.
At the risk of offending, I’ll say that I don’t use clever DI frameworks at all, even the ones that let you freely intermix with explicit passing. The reason is similar to my rationale for not using auto-mockers: it lets you do too many things one oughtn’t do.
So we’ve looked at one case of moving code we want to test to someplace where it’s easy to test, but this isn’t a technique that’s restricted to service endpoints, or even to framework environments.
Another framework setting first: apps with UI’s nearly always use a framework. The problem is exactly analagous to the endpoint situation. I don’t want to test it with the UI running. And the answer is the same: make OBL not use the framework, move it elsewhere, test it there.
Sometimes, the problem isn’t a framework on top, it’s a library on the bottom. Direct expressions of SQL in your code make that code untestable w/o a live and populated DB, for instance.
So here, the rim class is at the bottom, and its single responsibility isn’t de-frameworking but de-databasing the interactions.
Such approaches usually work by returning answers as generic or custom containers. If those containers can be hand-loaded instead of DB’d, then we can test our OBL without having a database handy.
Now, there’s one more, very very simple case of this, involving neither frameworks nor libraries.
The single most common question from would-be TDD’ers in their early efforts: how do I test complex private methods?
Do you see the answer? Put them someplace where they’re not private and test them there.
In the Before, we have a.public() calling a.private(). In the After, we have a.public() calling it’s private member of type B’s b.public(). The OBL is in b.public(), where we can test it to our heart’s content. Clients of A have no idea that such functionality is even available.
So you can see what I mean now when I says today’s idea isn’t really a refactoring. It’s a strategy, with a variety of tactical approaches to accomplish it.
By way of calling out themes: 1) The code works for you, you don’t work for the code. 2) Most refactoring is about re-arranging, not eliminating. 3) The most important code to test is "our branching logic". 4) Don’t let anything keep you from testing what you want to test.
The GeePaw Podcast
If you love the GeePaw Podcast, show your support with a monthly donation to help keep the content flowing. Support GeePaw Here. You can also show your support by sending in voice messages to be included in the podcasts. These can be questions, comments, etc. Submit Voice Message Here.