Double Trouble
I have been thinking about two kinds of twos this week — doubles, and programming in pairs. The first of these have given me no end of grief over the past week or so. The latter, on the other hand, has been a total joy. So something of a mixed bag.
The good will be the subject of a future blog post, but today I’ll address the bad, which was really encapsulated* for me in the ‘takeaway’ weekend challenge. This was to build a program to enable a customer to place a takeaway order, and receive a confirmation text when their order had been finalized. We used the Twilio gem to send the confirmation texts; naturally I was devastated to find out that you can’t send texts to numbers with a free Twilio account unless that number has been verified first, as this shattered my dreams of sending ‘hilarious’ fake order confirmation messages for extremely bizarre meals to all my friends. This did not, as the header shows, stop me from constructing deranged and erratically priced meals for myself (look, it’s lockdown, you have to make your own fun). It was also interesting seeing the default meals I chose, which, as well as a baked potato and the world’s fanciest angel delight, included spaghetti bolognese and a ham sandwich. Apparently my dream restaurant is a caf from about 1987, which is a strange choice for a vegan.
The biggest headache I experienced with this project, though, was courtesy of the dreaded doubles. Doubles are mock instances of a class created in spec files to mimic the behaviour of that class, so that we can test how the class we’re speccing (is this a word?) interacts with them. We don’t want our tests to concern themselves with the behaviour of other classes themselves — that’s the responsibility of their own spec files. We use doubles so that, if there’s a bug in the class being mocked, it doesn’t affect any other test suite. All of our classes can then be tested and written independently of each other. We can hard-code the response a certain method should yield from an object into the double, because we’ll test whether that method actually does yield that response when we’re testing the object itself.
TENUOUS ANALOGY ALERT: Think of it like a football team. To train your strikers, you might shoot balls from different places and at different speeds into the final third or penalty box, for them to practise taking. This shooting would mimic the behaviour of the midfield, but it assumes that the midfield will do what it’s supposed to do — get balls in to the final third. And it might be the responsibility of another part of the coaching team to similarly test and train the midfield in isolation. Evidently in the game itself you will need both the midfield and the forwards to perform, but in terms of training, these functions can be separated out. This approach would also give us an indication, if we keep failing to score, as to whether our problem is our strikers or our midfield.
(I look forward to hearing about how I’ve woefully misunderstood the nature of football, but I hope this shaky comparison makes at least some degree of sense).
So, to my takeaway project. I had a few classes, which included a Menu class, an Order class, and a Dish class. The Menu object, once created, would store an array of Dish instances, which would each have a name and a price. A ‘#pick’ method within Menu would check if a dish was actually in that array, raising a ‘not on the menu’ error if it wasn’t, and returning the item if it was. Each Order instance would store the Menu object it was ordering from, and when adding items with #add_items, would first check whether the order had already been closed, and if not call #pick on the menu with those items as arguments, and push them to an array stored within in the Order using #add_items. It would then update the balance of the order to reflect the new total using #get_balance.
To test my order class, then, I needed a mock menu that would respond to ‘pick’ and return mock Dish objects; I would check that #pick was finding the right items, and returning an error if they weren’t there, within the menu spec. The order spec would just be testing its own behaviour — that the balance was being correctly updated, and that items were being pushed to the array. It only needed the mock menu to give it something to push.
All of my tests were passing — except, that is, the last one. Whatever I did, I kept getting the same failure. Rather than updating the balance by 7.95, which was the sum of the prices of my two dish doubles, it was only updating it by 6.5. I was baffled; where on earth could it be getting 6.5 from? I followed all the debugging tricks I could think of, getting my test to puts the order balance, the sum of dish1.price and dish2.price, even the outcome of the get_balance logic on an array of [dish1, dish2]. I even ran through a full feature test in irb with the exact same values as I’d assigned my doubles. It all worked perfectly. So why was my test failing?
The answer, of course, was in that horrid little menu double. Congratulations if you’ve spotted it already. I’d made my menu double at an earlier point, when I’d only had functionality to add one item to an order at a time. So I’d only written it to respond to #pick by returning dish1, because this was all I needed to test at that time. Being a double, this was then the only thing it could respond with. So when my test ran, all that would ever get added to the menu would be dish1. If I passed 7 items to add_items, all different, it would certainly add 7 items, but they would all be dish one. Not a very interesting meal. As such, my test wasn’t working because the balance being added wasn’t dish1.price + dish2.price, it was dish1.price * 2. If I wanted to test for when pick returned multiple items, I would need to either rewrite the menu double, or write another one specifically for that purpose, able to return more items. But crucially, one double could not do different things — its response when asked to ‘pick’ something would always be the same.
I spent the better part of an hour looking at this problem, writing and rewriting my code and generally tearing my hair out. Eventually I decided I had to leave it alone, and would ask the coaches on Monday; the solution then came to me a bit later as I was knitting and watching TV (always true to my personal brand). A lesson in learning when to walk away from code (when you’re not getting anywhere, as a general rule) and defer to help, trusting your mind to keep running over it in the background in the meantime. A lesson too, in trusting your intuition. I knew irb couldn’t be getting it ‘wrong’, and I knew my logic worked — the problem therefore had to be somewhere in the spec file, and I ought to have looked at the double a lot earlier, rather than convincing myself that the code in my class file must be wrong. My code was wrong, it’s just that it was my testing code. RSpec is not an infallible, omniscient God, it isn’t always ‘right’; it only tests what you ask it to test, it can only see what you give it, and you might be giving it and asking it to test the wrong things.
In short, doubles do what you tell them to do. Exactly what you tell them to do. So be careful what you tell them to do. Or, to quote Ben Parker — with great power, comes great responsibility.
*note to self: scope here for coding puns**
** note on note to self: puns on scope also a possibility***
*** (note on note on note to self: maybe puns on self too?****
****note on note on note on note to self: is this an example of recursion?