In a data-rich environment, we can use the Builder concept to make DSL’s for our Making application.
This often makes testing the hard business core of our code both faster and easier.
Folks, I love sharing my geekery with you. For me, it brings much comfort. I hope, tho, you’ll join me in working for change that isn’t just about code, but about the larger world.
Black Lives Matter.
We’ve spoken in the past about using our codebase to do more than one thing. We always use it to create our shipping app. But we can and do use it for an app that improves our making process. We call that the making app.
When you’re running your microtests, it doesn’t necessarily look or feel like you’re running an app, but you are. And that app uses much of the same source-code that what you ship does. It just uses it in a very different way.
Some applications manipulate a whole lot of intricately connected data.
In the world of healthcare, for instance, an app might have literally dozens of classes, all rooted to and connected with a Patient. And the business logic is quite thick, with many cases and variants.
Suppose we wanted to test a key part of our logic, around getting permissions for medical procedures involving minors. To do that, we care about just a few things: the patient’s birthdate, the medical system they’re part of, the current date, and the procedure’s minority rules.
The naive approach to testing the variants here looks like constructing all these objects, hooking them together, and calling some code. There are about a half-dozen cases, so we’ll do this all a half-dozen times.
And there you see a cost begin to emerge: those objects are complicated, with many fields, most of which are entirely irrelevant. They must be connected, with ID’s or keys that match each other.
There’s a lot of noise there, and the signal is in small differences in large blocks of data. The making creeps its way into being unpleasant. And when that happens, when tests are painful, you know what we humans do: avoid them.
The low signal-to-noise is especially true if you’re using automockers to simulate data sources or you’re actually literally writing to a database. Please start by killing those two practices off. But if you’ve done that, you’ll have made it better, but not enough better.
Ultimately, objects have to be made,with their fields, and connected.
There aren’t two ways around that. So if you see tests as arrange-act-assert, what you’ll see is that your "arrange" is comparatively huge, tho your act and asserrt are 1-liners.
Now, the quick thinkers will have already jumped to a key potential solution. "We need some way to make a default patient, then we just change the fields we want changed." That’s an excellent idea, and a strong starting point, but it’s just a start.
See, a real Patient doesn’t have fields you can just arbitrarily change. There are methods in the API that can do this, but they’re themselves messy and complicated, and often require even more data to be poured into our "arrange".
Further, even if we solve that problem for Patient, we still have to solve it for the other objects, and we have to get them all linked together, too.
Enter the ScenarioBuilder (SB).
This class is a specialist in turning descriptions of scenarios into concrete objects that represent that scenario. The key: it gathers all the descriptions up front, and only does the construction at the very end.
You create an SB, then you say things to it like: "today is 2021-03-21". "gimme a patient". "put her in Alaska Medical System", "make her 12 years old", "gimme a scrip for morphine". The SB doesn’t do those things — not yet — it just collects the description.
Eventually, you say "go", and it makes all the objects needed to fulfill that description. It uses default values for anything you didn’t override. It connects the patient to the medical system. It grabs a physician for a scrip, and so on.
Now, to flesh this out a little more, let’s talk pros and cons.
There’s a lot to this, and it will definitely take you some effort, so we need a sense of what’s good or bad about it before we dive in.
- Pro: It’s part of the Making app, not the Shipping app, so we have complete control over its API. We can replace forty lines of boilerplate to give somebody a procedure with a single API call: enema(). There’s zero shipping use for this, but its invaluable in making.
- Pro: We can provide API’s that completely hide all ID/key manipulation. If you make a patient, and you make a doctor, and you make a scrip, the ScenarioBuilder will just assume you wanted them connected.
- Pro: If your language is capable of it and you’re good at it, SB calls will be easily legible to others, including others who aren’t programmers. (Kotlin’s extension methods and end-lambda syntax are wonderful for this.)
- Pro: A change in the structure of a Patient might break the ScenarioBuilder, but it won’t break the scenarios. Change how SB makes its stuff, and your scenarios are still valid.
- Con: It’s a fair amount of work. You can start small, but in truly data-rich environments it will grow pretty rapidly. You may wind up having to write tests against the SB itself, for instance.
- Con: You must rigorously define your "nominal" values, the defaults the SB will use unless you tell it not to. You’ll have to share those with your team, and you’ll almost certainly want some pretty-printing capability, too.
- Con: Because it’s not part of the shipping app, you need it to be valued. That is, doing a half-assed job isn’t really an option. It has to be much easier than the old way, quick to explain to noobs, and quick to flex under change.
- Pro/Con both: It’s a custom jig for a custom domain. That’s a pro because you can fit it really tightly. It’s a con because it won’t move easily to other domains, even if they’re conceptually nearby.
This has gone on a while, so let me just shoot a couple of quick pro-tips and let you go.
Think about the ordering of your descriptive statements, which is best left somewhat loose, but can benefit a great deal from "what’s already said": if you said it’s 2021-03-21, do you want that to only apply to the object you’re describing, or from that point on?
You may have to create simple data classes to hold elements of a description. That’s because you’re not implementing the description until the very end.
You can sometimes benefit by extracting a long very-detailed description into a method and giving it a handy name for other tests, akin to using setup helpers. "guyWithBrokenLeg()" or some such.
That’s it for now. But just for now. Let me foreshadow a little: You see, I wanted to write about this so we can talk about edging our way into a very hard problem: modern microservice apps distribute their business logic all over hell’s half-acre.
There are ways to approach this, but they first require you to think in terms of two apps in one source base, then in terms of scenario builders, and then, well, we’ll see.
If you love the GeePaw Podcast, consider a monthly donation to help keep the content flowing. You can also subscribe to get weekly posts sent straight to your inbox. And to get more involved in the conversation, jump into the Camerata and start talking to other like-minded Change-Harvesters today.