Prevent complex test data from spiraling out of control by going to builder & custom comparator early on.
The push-to-small, coupled with SOLID, coupled with things like third normal form, all lead us to a place of wanting to compose domain objects into potentially very rich dependency graphs.
A card in a address-tracking subsystem sounds at first blush like a class with about 5 simple strings in it, and we certainly start there when we first approach it. But as we proceed, we thicken that object quite a bit.
Sample thickenings: validated zip, phone, etc. Support for international addresses. Support for names that come in different formats than whatever your first culture uses. Support for history. Support for multiplicity of address, or name, or phone.
(aside: please be clear, I never start with all that crap, nor should you. Guard very closely against attempting a contact card that is once and for evermore the contact card to end all contact cards. Focus on your customer’s current need, one small need at a time.) and you can see with these thickenings that our dead simple and naive first swing — a wonderful thing we shipped that we’re proud of — quickly turns in to a cluster of related objects. The simple idea of "contact" has become a directed dependency graph of many objects.
The great Gibbon wrote an astonishingly readable (from the 1770’s no less) history of the roman empire, and he frequently used the word "insensibly", which wasn’t a word I was familiar with. The word just means "all unawares". I slid insensibly towards a fondness for deviled eggs just means that I did so without ever even noticing how attached to them I was becoming.
[ed. Note: please do not send him more deviled eggs. It’s just gross, now.]
We slide the objects we create insensibly towards object-clusters all the time. The sliding itself is awesome. The part where it’s insensible, tho, that’s death to your geek performance.
Why? Two reasons. 1) data setup, 2) test validation. They both get inch by inch more difficult as we thicken our objects. And we sit there, face all covered with that awesome mustard-y yellow crumb stuff, not even noticing that we’ve just incited a monstrous belly-ache.
So. If my code has to tweak some part of a thickened object, my test has to either make that thickened object or make that sub-object.
If it’s just the sub-, great, we’re good to go. But if the owning object has functions and those functions depend on the sub-object’s behavior, it gets a little messier. Now i’m making the owning object, and all its subs. And i’m testing the owning object, and all its subs.
If you’ve been inhaling programming theory, and you should be, you’ll point out that were the owner and the component well-factored, this won’t happen. And that to me seems like an entirely accurate statement, tho someone may give me counter-examples.
But remember, we work for a living. And if we’re any good at it at all, we work for a living solving problems we haven’t solved before. That means we’re constantly introducing new function, new classes, new everything. Programming happens in time.
Not only is it low odds that my new code is well-factored in the beginning, it’s also low odds that the existing code i’m adding to is well-factored for my addition in the beginning. The well-factoredness of the code is just not a thing I can wait for before I start.
So, back to the tip. Builders and custom comparators. A builder is just a class or framework that lets you make new objects without fully describing them. A custom comparator is the same, except it lets you compare two objects without caring about certain aspects of them.
When I am changing how the contact interacts with its phone field, I am utterly and completely indifferent to how it interacts with its contact type, its address, its history, its editor, its — it goes on an on. That indifference means a) I don’t want to specify all that crap when I make a contact, and b) I don’t want to compare all that crap when I write assertions.
Solution: a generic one-size-fits-all-test way to specify a default contact and override just the parts you want. A custom one-size-fits-my-current-tests way to specify the comparisons you care about.
If the contact is a simple object, we can usually get away with some sort of default value scheme, either using literally default arguments, alternate constructors, or a helper function in our test that holds them all steady. All of these are good if they do the trick.
When contact becomes thick enough, tho, we have not only sub-objects in our fields, but often sub-sub-objects, too. Here, the simple approaches above just won’t cut it.
The case is similar with comparators. Early on, even when we write custom comparators, they can be simple one-size-fits-test, and work like a charm. When things get thick enough, tho, it’s likely to get owwie.
So, i’m already going on too long, what to do?
Start by learning what builders do and how.
Next learn any handy cool language support u might have for them, like fluency or extension methods.
Third, learn how to write comparators and make them do anything you want.
My recent case on this is already mentioned, building golden datasets that pretend to be my app’s variety of upstream data sources. The datasets are richly cross-connected. Specifying those directly is nearly impossible w/o a builder. It’s dead easy with one.
The builder interface I use lets me not just supply the cross-linked data, but to express it many times more simply than the app itself (or the live upstreams) do. Tests go from having huge setups to tiny ones, from having lots of noise to "only important words used".
The deepest part of this pro-tip? Your tests must be tended exactly as much and exactly the same as your shipping code. As soon as they tricky to write, wonder about investing a few hours in untricky-ing them.
Don’t let insensible complication creep in to your code, of course, but don’t let it creep in to your tests, either.