Today, I'm going to tell you something about "Test Implants". Sounds a bit scary, but in fact it isn't at all. I recently wrote about a technique on how you can improve brownfield code with tests. In fact, I showed that it's surely possible to do some TDD on an existing codebase with the help of simple "Mixin Contexts".
This time I'm going to share another technique which I found to be very helpful in brownfield engineering scenarios. Obviously, I named this technique "Test Implants". Test Implants are no solution to "untestable" code, but help you to find out the parts which need to be addressed.
Effectively, every test implant I wrote made me rethink on how the code might be restructured to fit all requirements. For me, test implants are a simple, feasible way to get interaction tested without essentially restructuring the organization of the code. I personally use test implants for over a year now and found it very useful most of the time.
I'm going to show you test implants using a little fictional story. No intentions behind that, the story is just a cake with topping alongside the coffee. The title of the story, obviously, is "The Quest Of The Test". Enough introduction, let's just start.
I'm going to tell a little story about legacy code, bugs and testability. You, my dear reader, will be put right into the fabulous adventure of finding out what a test implant is. Enjoy.
A typical Brownie
It's a regular, rainy Thursday morning, five past nine. As always, you're a little late to work. You quickly pull of your coat while switching on your monitor and logging in with your left hand. It's not that your password isn't complex. It is fairly complex. It's the virtuosity of your memorized typing of course that makes it possible to login single-handed.
Once you're seated, you see bad news in your inbox. "Bug in Chronograph!" says the title of the mail. What? The chronograph? The legacy code you just have become the official maintainer for?
Justice has left this town. That's for sure.
Ok. Ok, ok. While you concentrate to breathe in regular intervals again, your brain starts to send plausible signals: "Get me a coffee. Then we'll fix this bitcrap, buddy". Aight! You put in a Nespresso "Volluto" for starters and hit the button. While the coffee machine is at your command, you open the Chronograph files. You happen to recall that it's using TimeServer, an almighty third-party DLL. It contains classes and methods to fetch a global standard time from internet and translate it to local time.
Bing! Coffee done! "A morning with bugs and coffee. The usual suspects in my ordinary life." is the most original thing what you are able to tweet right now about your adventure. Ok. Social shit done as well. Start working. Now.
Sources refreshed, project opened in Visual Studio, table light dimmed. You're just rereading the code of Chronograph, which has been written in "ancient times". Obviously without any unit tests at all:
public class Chronograph { private CultureInfo _culture = CultureInfo.InvariantCulture; public bool IsDirty { get; set; } public LocalTime SyncWith(TimeServer ts) { var localTime = new LocalTime(); if (IsDirty) { TimeEntry te = ts.GetTime(); te.Apply(_culture); localTime = te.ToLocalTime(); } else { TimeEntry te = new TimeEntry(localTime); te.Apply(_culture); localTime = te.ToLocalTime(); } return localTime; } }
Hmm. This code smells to have bugs. You're staring at it and think "Oh my. Look at the dupes. Rookies." If you wouldn't just slurp your coffee while browsing code, your left hand would be ready to serve a generous face-palm. Your excellent nose didn't mislead you, as you soon realize while reading the bug report in Bugzilla:
Bug Nr : 0815 Title : culture is not updated when clock is in sync with server Component : Time Sychronizer (Chronograph) Description : While the local time is in sync with server, the culture needs to be "updated" regularly. It happens not to be the case. The time server API has a specific function to update a time entry with culture: TimeEntry.Ensure(CultureInfo). Use this function to keep culture up to date.
Good to know that a professional like you is taking care of this nifty bug. While browsing through TimeServer API, you realize that TimeServer, TimeEntry and LocalTime classes are all sealed. Without interfaces.
Your mouse pointer moves the scrollbar slow. Very slow. You try to cheer up yourself: "An API style I've seen a hundred times at least. Nothing shocking so far." You know the truth is different. You wished to stay at home today. Now you're here. Right in front of this artfully crafted code. Not.
Riiing! 10 AM! Stand-up time! "Come on, McFly, wake up!" is the flash of thought right after the alarm window popped up on your screen. Lock the box, put off the mug, off to 3rd floor! Since your department has moved last year from 3rd to 1st floor, these dailies happen to have the notion of helping the staff to burn calories. You hurry up to entrance. "Yeah. Right." - all elevators blocked. Stairs, as always.
TDD is En Vogue
Arrived at 3rd floor you see all devs listening to the lead developer. You don't like him, yeah. Nonetheless, this time it's as if an important announcement is about to happen. "Will he retire?" is all what you can imagine to be a positive thing right now. However, surprise is ahead.
From now on, the management and the entire organization will support and encourage to develop literally everything with TDD. No code will be put in production which is not TDD'd, no bug will be resolved without proper tests using test-first method. Period.
It's Christmas and Eastern altogether. Did he really say TDD? Yes, he did! Life is a roller-coaster. After having reached the grounds this morning, you're now back on track. Finally TDD. All those arguments you had in kitchen, all those emails you wrote to colleagues with links to Uncle Bob and Kent Beck, all those code reviews and little wars about test frameworks and CI integration have come to an end. Whoohoo!
Ok, great stuff! Finally your company is on the right path. Right after daily you hurry down to your desk again. Now let's do some bug hunting! You start to examine the class signatures involved in that nasty bug again:
public sealed class TimeServer { public TimeEntry GetTime(); } public sealed class TimeEntry { public TimeEntry(LocalTime lt); public void Apply(CultureInfo ci); public void Ensure(CultureInfo ci); public LocalTime ToLocalTime(); } public sealed class LocalTime {}
Wow. Sealed. The Dev who wrote this must have got a Kiss from a Rose. Looking back at the original Chronograph class, it's crystal-clear that the else branch needs to call Ensure instead of Apply. Pretty easy. But wait! How to write a test for this tiny typo? You look back into your digital toolbox: Only NUnit and Moq available. Hmm. You stop for a minute and rephrase the question in your mind: "How to write a sensible unit test beforehand to ensure that the bug is fixed right after you have just replaced a single word?"
The Quest Of The Test
You wouldn't be a geek if you wouldn't consider it as a duty to find an answer to this question. With years being in this business, you very surely know: There's coders, there's hero coders, and - there's you. The metaphorical ant of all coders. You'll sit here, with your coffee mug on your left, and take the quest. Duty is calling, comrade.
First, you just do the regular easy stuff. That is, get NUnit referenced, build up a skeleton of the test class and write up your environment to be "ready to assert". No minute later, you're staring at this code fragment:
[TestFixture] public class When_chrono_is_in_sync { [Test] public void Then_culture_is_ensured_with_time_entry() { var ensureWasCalled = false; var ts = new TimeServer(); var chrono = new Chronograph(); chrono.IsDirty = false; chrono.SyncWith(ts); Assert.IsTrue(ensureWasCalled); } }
So far, so good. Now the kindergarten is over. Right now is the time to separate real men from boys. You need to find a plausible and sensible way to express the expectation, that te.Ensure() is being called instead of te.Apply().
It's time for another coffee. Volluto doesn't do any more. This exceptional situation cries for exceptional coffee. You put in the "Roma" tab into the machine and let the mug do its job: awesome coffee storage. No milk. No sugar. Outlook off, Browser off, Tweetdeck off. You literally feel the scientific aura now.
"Wait a minute..." you think. "... scientific? Science. Yes, science is a good catch right now. I need to express a function in order to put it under assertion.". Express. A. Function.
Eureka!
That's the key! You'll go for a lambda expression. You need to define the function which actually calls the Ensure method and then assert its proper usage simply by checking whether this function has been called. That's like implanting a lambda into the existing code. You quickly smash in some prototypical code to verify your solution path:
public static class TimeEntryImplant { public delegate void Ensure(CultureInfo culture); }
That's the delegate. Let's weave that into Chronograph in order to be able to utilize it:
public class Chronograph { private CultureInfo _culture = CultureInfo.InvariantCulture; private TimeEntryImplant.Ensure _implant; internal Chronograph(TimeEntryImplant.Ensure implant) { _implant = implant; } // ... }
And finally heading on to extend the test:
[TestFixture] public class When_chrono_is_in_sync { [Test] public void Then_culture_is_ensured_with_time_entry() { var ensureWasCalled = false; TimeEntryImplant.Ensure implant = culture => ensureWasCalled = true; var ts = new TimeServer(); var chrono = new Chronograph(implant); chrono.IsDirty = false; chrono.SyncWith(ts); Assert.IsTrue(ensureWasCalled); } }
"Yeah, right." Looks good so far. The strategy now is hacked in. You feel good, you feel confident. Nonetheless, you still feel the need to reiterate your solution path again. The pro is working here. Swiftly, almost gracefully, you stand up from your seat, followed by two elegant sidesteps towards the whiteboard. On a small space on the upper left corner you rewrite the essentials steps again:
- Declare & define expression containing the new functionality.
- Use the expression within assertion to get the test red.
- Implant the expression within class under test to get the test green.
Freddy Fredpecker
"Hey, hey, heeeeeeey!" you hear a dark, arrogant and unemotional voice on your back saying with low voice. It's Fred, your "colleague". No. Collegue is not the right word. Fred is an arrogant Egomaniac, who coincidentally happen to work at the same company as you do. To make it even worse, Fred is the self-proclaimed "Testing Expert" in your department. He actually hasn't heard anything about MSpec, he writes "Unit Tests" with database usage and he is very keen to rigorously punish every developer with irony and sarcasm who is not obeying the almighty AAA-rule. Testing Expert, you know.
You turn your back slowly and see him sitting on your desk. "What an ignorant, low-life piece of crap he is" you think as you see him tipping with his dirty fingers on your beloved Octocat Mug while swinging around in lay-back mode on your swivel-chair.
"You seem to have a faible for testing, aren't you? Buddy, realize testing is not about colors or expressions. I see..." Fred continues while having a glimpse at your test code, "... you're trying to test something with lambda. Man, I knew you're crazy. Like 'Adam Sandler-Crazy'. But I didn't knew you're dumb as well. Like 'Jim Carrey-Dumb', y'know?" He harshly puts your mug back on your desk while moving his butt off your chair.
"Fred," you reply, "ehh... eh... well... em, you're right. I'm just trying out something, y'know? It's wrong shit anyway, so don't care about it. Well, y'know it's hard for me as an 'average' coder to get into this testing stuff. I'm experimenting a bit, that's all.".
You know you need to get rid of him. Now. You put that ordinary, demotivated Looser-Look on your face and stare at Fred. Deranged. - "You better get a life, kid." mouns Fred and slowly moves out of the door. You follow him, with a little, respectful distance, grab the knob of the door and close it silently.
Two Strikes To Green
"Phew... at least I got rid of this low life looser". You quickly move back to your desk and grab the keyboard. You're so close to the solution, only a few keystrokes away. The test is red. You know you could go green by just invoking _ensure function you injected through Chronographs constructor.
However, that's not the idea of this game. The Idea is to ensure that the real Ensure method of TimeEntry has been called. You lean back for a minute, then decide to slowly hack in an extension method to TimeEntryImplant:
public static class TimeEntryImplant { public static void EnsureImplant(this TimeEntry entry, CultureInfo culture, Ensure implant) { (implant ?? entry.Ensure)(culture); } }
Yep. You pause. Just staring at your code. It's these little moments, these little seconds in your poor hacker life you're loving to have. You made it. And it looks beautiful. You just got your very first test implant.
Grande Finale
Now let's head on to the finishing move! It somehow amazes you that you're able to switch back into rapid fire mode so easily after being in the 'code monkey stares at code'-position. Rolling back with yout chair to your desk, you almost spilled your coffee on your beloved keyboard. Almost.
Ok. Concentration. You already know it. Victory is only one single line away. With the heart of a winner, a generous and confident smile in your face, you open the Chronograph class and change the line you knew you needed to change from the beginning. You locate your enemy:
te.Apply(_culture);
Squash this buggy line away and place in your implant:
te.EnsureImplant(_culture, _implant);
Save. Run test. This is the moment. You stare with eager into test results window while your stuff compiles. You're sweating, altough you haven't moved your ass even an inch away from your computer.
Ok, runner starting... testing... green!
Victory!
You lean back, grab your mug, sip a little bit, switch back to code to see the beauty of the complete Chronograph class:
public class Chronograph { private CultureInfo _culture = CultureInfo.InvariantCulture; private TimeEntryImplant.Ensure _implant; internal Chronograph(TimeEntryImplant.Ensure implant) { _implant = implant; } public bool IsDirty { get; set; } public LocalTime SyncWith(TimeServer ts) { var localTime = new LocalTime(); if (IsDirty) { TimeEntry te = ts.GetTime(); te.Apply(_culture); localTime = te.ToLocalTime(); } else { TimeEntry te = new TimeEntry(localTime); te.EnsureImplant(_culture, _implant); localTime = te.ToLocalTime(); } return localTime; } }
You did it. You squashed the bug. TDD. Test-First. Brownfield. What a victory.
After this tremendous win, you surely got excited enough from your test implant to tell all your mates about it. In your office, though IM, Facebook, Twitter and whatever medium you have ready. For hours and hours, you reiterate your journey.
Through all this excitement, you even get your hands on other, similar pieces of code and try using the implant technique. It's easy, it's expressive, it just works.
After all these exciting hours, you literally didn't notice how fast time flew away. It's 9 P.M and you're still in office. "Oh shit, I'm supposed to meet with my honey at 10!". You grab your jacket, lock your box, switch off lights and slowly move stairs down to basement. Time to leave with pride. With the 'test implant' feather in your cap you walk out of the office and step into the bus.
An hour later you're finally at the bar to meet your sweetheart. She steps in, beautiful as ever, smiles and quickly skirrs into your arms. This day can't get better. She gives you a long, affectionate kiss - willing to stop at nothing.
All of a sudden you stop the kiss, smile at her at your best and whisper: "I love you, hon..." while thinking... "OMG, I forgot to check-in!"