So, so much of good system design is abstracting out complexity. So, so much of good testing is understanding where the complexity is and poking that with a flamethrower until you decypher as many interesting things about that complexity as you possibly can.
Yes, that's a lot of badly mixed metaphors. Deal with it.
I'm taking my team of testers through learning Test Driven Development using the Payroll Calculator example from my course Coding for Non-Coders (https://www.ministryoftesting.com/dojo/courses/coding-for-non-coders-jim-holmes). The problem walks people through creating a small bit of code to compute wages for an hourly worker based on the hours they worked and their payrate. The worker may have some overtime if they worked over 40 hours in one week.
Simple problem, familiar domain. Hours, rate, determine how much a worker gets paid before deductions.
Right now we've finished standard time and have six total tests (three single XUnit [Fact] tests and one data-driven [Theory] with the same three test scenarios).
The "system" code right now is this bit of glorious, beautiful stuff below. Please, be kind and remember the context is to show testers a bit about TDD and how code works. No, I wouldn't use int for actual payroll, m'kay?
public class PayrollCalculator {
public int ComputeHourlyWages(int hours, int rate)
{
return hours * rate;
}
}
We were getting ready to move into writing our first test for overtime. At this point, one of my team members jumped ahead to ask about adding in a separate method for computing overtime hours. We haven't written any tests yet, but this was absolutely worth heading off on a discussion!
The intent of my tester's question was if we should make the system work like the snippet below--some hand-wavy psuedo code is inline.
public class PayrollCalculator {
public int ComputeOvertimeWages(int hours, int rate) {
//calculate ot wages
return otWages;
}
public int ComputeStandardTimeWages (int hours, int rate) {
//calculate standard wages
return standardWages;
}
}
Splitting calls to compute separate parts of one overall action may seem to make sense initially, but it's far more risky and complex.
What are we trying to do? We're trying to figure out what a worker gets paid for the week. That's the single outcome.
Think about some of the complexities we might run in to if this was broken into two separate calls. Think of some of the risks that might be involved.
- Does the order of the calls matter? Do I need to figure standard hours first, then call the ComputeOvertimeWages method? What happens if I call overtime before standard?
- Do I call overtime for only the hours above 40?
- If the worker put in over 40 hours, do I call standard wages with just 40, or will the method drop extra hours and just figure using 40?
- Does the code invoking these calls have to keep track of standard and overtime hours?
- What happens if in the future we change the number of standard hours in the pay period?
As a system designer you're far better off abstracting all this away from the person calling your methods.
One simple method, and you hide that complexity. Just give the person calling your API what they want: the actual wages for the worker.
This same concept applies if you're dealing with complex workflows and state. Let's say you have a five step workflow for creating a new job bid, and you need several critical pieces of information at each step.
Don't make your users or callers figure that complexity out. Give them one place to start the workflow, and ask for everything you need up front. Then YOU figure out how to handle the complexity and hide the goo from them. Just return them a shiny new job bid.
One interaction, one nice result. Easier to write for your consumers, far easier to test, too.
Someone years ago spoke of making it so your users could fall into the pit of success. Do more of that, and less of pushing them into the pit of despair. (I'm throwing out a Princess Bride reference, not one to Harry Harlow...)