This presentation was recorded at NodeConf Oslo 2016 on June 4, 2016.
Test driven development is a soft skill
The purpose of this talk is to give a quick, light-on-its-feet introduction to an unorthodox discipline of test driven development (“TDD”) that I’ve termed “Discovery Testing.”
You may enjoy the content of this talk if:
- you’re interested in testdouble.js, but don’t want to start by diving into its relatively exhausting documentation
- you’re familiar with TDD and for whatever reason didn’t have a tremendously positive experience with it
- you’ve ever been confused by styles of testing that make heavy use of test doubles (e.g. mock objects)
Related resources
Here are a few links of things either referenced in the talk or relevant to its content.
- testdouble.js
- teenytest
- The unusual spending kata
- A 4-part screencast series which demonstrates discovery testing in Java with a Game of Life
- A comparison between td.js and sinon
- The google results for tdd failure
Transcript of the talk
[00:00:00] Today's talk is titled Happier TDD with testdouble. js, which is a library that I've written for mocking in JavaScript. I come from a company coincidentally also called Test Double. My name is Searle, so you can find me on Twitter there. Find Test Double on Twitter there. Why bother with test driven development is a question that I think is completely worth asking.
[00:00:20] It's fallen out of vogue in the last couple of years, and it never really caught on in a serious way in JavaScript, and I think a big reason is the tooling was never all that awesome. But if you're not familiar with test driven development, it's really easy to teach it at the basics. First you write a failing test, which is red, and then you go green by writing, making that test pass.
[00:00:37] And then you go purple by refactoring the test. I don't, I actually don't know what color refactoring is, so I picked purple. But the reason that I do it is primarily it, the benefit of focus. By writing the test, I know what I need to do next, and I'm focused on just that one task at a time, not thinking about everything all at once.
[00:00:54] The second benefit that I like is a sense of progress throughout the day. So instead of thinking about I've got this gigantic story card, and I'm just gonna change shit until it all works out. And maybe I'll feel like at the end of the day, like I got nothing done. I hate that feeling, and test driven development gives me iterative little bits of progress throughout the day.
[00:01:07] That's great. Three, blank page syndrome. I'm a really high anxiety individual, and so when I see a big blank screen, I know I got a lot of work to do. I just don't know how to do it, and I freak out. Test driven development has given me a way to break problems down. And four, it slows us down. It, you move more slowly, which is actually a good thing.
[00:01:23] Our culture tells us faster equals better all the time, but that's not necessarily going to result in better, more thoughtful, more robust designs. And sometimes slowing us down stabilizes us. So that's what I like about TDD. Of course, a lot of people react what I thought that TDD was about note defect code and having lots of tests.
[00:01:40] In fact, I was mentioning earlier, if you Google TDD failure, I'm the top result. So I'm this is a rebellious talk. I'm pushed back against some traditional TDD dogma 'cause I do things a lot differently. All right you could, I guess you could say that I take TDD for its off-label use.
[00:01:54] Like these are like side benefits, but I actually. view them more favorably than necessarily thinking about regression testing, per se, as being the primary benefit for TDD. In fact, I start to think like TDD is a soft skill. If I was like, if I have zero anxiety and I like, have no risk towards a project, I tend not to practice TDD as much because I just don't need it.
[00:02:15] But when I have high anxiety or there's a lot of risk and uncertainty or if I'm learning a new thing, then I lean on it because it provides me a lot of confidence. All right, so today we're going to do this thing. It's a code kata, like a little practice problem called the unusual spending kata that I designed to show people TDD.
[00:02:29] You can find it on github. It's here at testdouble's account at unusualspending. Of course, we're going to walk through the example today, but you can feel free to peruse the solution branch if you pull it down later and play with all the code. Alright, so suppose that you work for a bank and as a bank you guys have credit card members.
[00:02:46] And these card holders, we want to notify them when they spend more than usual on any given category of merchants. So if they spend a lot one month versus last month, we want to let them know, Hey, you're spending more than usual. It's just a little fringe benefit feature. You're just given that story card.
[00:03:00] And so your product owner comes to you and says, Hey, so given a user ID, I want you to look up this month and last month's payments. So that's the first thing I'd do. Then you got to group them by category, compare the total spending from month to month. Finally, summarize the high spending areas by emailing the cardholder a notification.
[00:03:18] So that's all the work that you have to do. So let's TDD it, right? So under a traditional simplistic TDD mindset, maybe we start by, defining our test subject. We're going to require this module and maybe just pick a user ID out, like 42, and then call the subject with the user ID.
[00:03:31] Great. We're off to a really good start. We've got two thirds of our tests done, the setup, the act. And now what do we do? Check the user's email. What about all the data in the database? How do we prime the test data? There's, this raises a whole bunch of questions and I'm right back to the start of having blank slate syndrome again, not helping me at all.
[00:03:47] It's nice to have the tests there, but it's not actually making me any more productive. Cause this is too big of a feature. I need to break it down somehow. So A lot of people do a little TDD activity, and they'll come to this conclusion, like TDD sucks, this is terrible, like this, it was fun as a toy, but in real life it didn't help me.
[00:04:03] So let's do a little mini design session to look at this from a different angle. So like at the top, yeah, we've got this unusual spending module at the top, and I like to ask the question, can I break this down by writing the code that I wish I had? If I'm lazy, right? So like I want to offload this workbook.
[00:04:18] For instance, the first requirement, I have to fetch payments. So given a user ID, I want to go fetch some payments. The second requirement, I want to determine the high spending. So maybe the payment fetcher can pass off an array of objects, like an amount and category. Third, I want to notify the cardholder.
[00:04:33] And so maybe the output of the second dependency is summary totals of high spending. So that the cardholder thing can just send the email based on that data. So that's I'm starting to shake out an initial design. What I really want is a test of just unusual spending and how it collaborates with Those three things.
[00:04:50] We're, I'm not testing that it all works, I'm just testing it breaks up the job correctly. So we're going to write a totally different kind of test. We're going to use a method called outside in testing, or I call it discovery testing. And we're going to use test doubles, which are like stunt doubles that stand in for all those things beneath us, because they don't exist yet.
[00:05:06] We need something to stub them out. And a test double here, we've made a a library called testdouble. js, it's up on npm as testdouble, so you just install it if you're doing Node. js. So in Node, we normally just assign it to a variable called td and in browsers it also has a browser distribution in there, you can copy or link it however you like and use it in browsers.
[00:05:24] I also, just for this talk, wrote a test framework for Node. js. It's a zero API test framework that I'm actually coming to quite a lot. It's real simple, you just put tests to some glob and then you can export a function or a whole object of functions and it's just really smart and it'll run all those things.
[00:05:39] And then give you tap output. So you don't have to worry about any sort of testing API like Jasmine, Mocha, or Tape. Now brace yourself. This is the test that we're gonna write together. It looks like this. It's a, whoa, that's a lot. What? That's too much to possibly just blab at you. So let's just hone in on what we can understand.
[00:05:55] We already know this. This is us loading the subject the same way we would have in a normal test. This is the user ID that we would have just picked out of thin air. And then this is us invoking the subject. So let's talk through the other stuff. The other three things up at the top, we've already identified those, that's fetch payments, determine high spending, and notify cardholder.
[00:06:11] So we've got those names, cause we like, in our little mini design session, we identified that those were going to be our three dependencies. Now the first line, fetch payments equals td. replace, and then a relative path from the test. td. replace is like a really cool dependency injection tool that's built into TestDouble.
[00:06:27] I point out, hey, at this module, I want a monkey patch require for the duration of this test. Then I want if the subject loads libfetchpayments, I want you to automatically replace it with an intelligently designed test double object, or function. And then after the test, require is restored to its default behavior.
[00:06:43] And so this is what facilitates the outside in, fake it to you make it style of testing. And so that means that I can preemptively replace these other things that don't exist yet, such that they can be required, even though those modules aren't there yet. And so there, that part's done.
[00:06:58] That's, our setup is now done. However, remember these are like fake functions. They don't know how to respond to anything yet. They'll just return undefined. So we need to configure these test doubles. So first I say like, all right when fetch payments is called with user ID, then call back, it's a callback API, a null error and string some payments.
[00:07:15] Similarly, when high spending is called, this is a normal function, some payments is passed to it, then just return high spending. These are just pure functions. And, to understand td. win, it's what we used to call stubbing the test doubles. We can say td. win is like configuring these different stubbings, and whatever you put inside of the braces here is a rehearsal.
[00:07:36] Just invoke it exactly like you'd expect the subject to to invoke it. And so it's really easy to compare the subject and the test. And then you chain off what outcome you want. Then return high spending if you're past some payments. Thanks. Yeah. So in this case, if I stubbed it this way, and then I called determine high spending with the right thing, it'll return a high spending.
[00:07:55] And if I call it with something else, I just get undefined. there's other chainables. Like you could then call back API, which has got a lot of cool options. Then throw, if you needed to throw an error or then answer, if you need the stub to have some kind of side effect, which is a little less common. So now we're all through all of the when stuff.
[00:08:11] That's great. And the last line here, just one more line in this test, td. verify notify cardholder dot email. User ID and high spending. So Verify works very similarly to Win. This td. verify ensures that the test double is invoked exactly like we expected. And here we de whereas when we rehearse, here we demonstrate this is how I want the subject to invoke it, with the exact arguments that I'm invoking it with.
[00:08:34] Here, And what's cool about Win and Verify is unlike a lot of other testable libraries for JavaScript, those APIs are completely symmetrical, so you don't have to remember which one goes where. They're, they have a lot of advanced configuration options and all the options are available in both modes.
[00:08:48] Like matchers and capturers and other series. Other options that are, very well documented but way too much to talk about today. And then the rule of thumb is, like you should probably only use verify sparingly. It means that your subject has a side effect and obviously returning real values is generally better practice than having side effects.
[00:09:05] JavaScript is not a pure functional language. We have side effects sometimes and you need to be able to TDD those as well. So that's why we support it. So here's our test. Good job, we wrote a test. Now, sneaking ahead a little bit, what we're really doing here is we're building a tree, like a functional tree of code, and one of the patterns that I've seen over the years is that it starts to look like an extract, transform, load pattern where the stuff on the left of each of these trees queries, goes and gets data from other systems, the stuff in the middle tends to be pure business logic, pure functions, and then the stuff on the right usually triggers some kind of side effect.
[00:09:38] So that's just an interesting pattern that I saw. The fetch payments thing, maybe it's gonna go get us months. Maybe it's gonna wrap the payment API and go fetch stuff. That high spending thing, maybe we'd break that up into something that categorizes payments, something that compares totals, and then something that filters out the high spending.
[00:09:53] And then the notify card holder, something to compose an email string, and then probably something to call some email service. Is how I'd probably break that up. I don't wanna get ahead of myself here. Let's just focus on this one test. Remember, part of the focus is that we can just get this one test to work.
[00:10:06] We don't have to solve that lower level stuff quite yet. So let's run npm test. Boom. Red. It's failing. We haven't implemented anything. So that's expected. First error we get is that lib fetch payments doesn't exist. And that sucks so one of our modules that we're replacing just needs to exist.
[00:10:21] So all we have to do is remember that TDD isn't as simple as red green refactor, at least not when you practice it outside in. TDD is really more like red, and then a different red, and then a different red, and then a different red. Then a different red, and then finally green, and then another red. And that feels like a lot of failure, but it's actually it's really cool, and that's when we talk about this little incremental progress that we feel.
[00:10:43] So your goal, when you're writing TDD, is either make the test pass with every change that you make, or change the message in some appreciable way. So here we're gonna export a function from fetch payments. Test is still failing, but we changed the message. Now it says, determine high spending doesn't exist.
[00:10:57] Real quickly, exact same flow, red, comes back, and now notifyCardHolder doesn't exist. It starts to feel like paint by numbers. We did design that test up front, but it's basically unwinding for us now all of the work that we have to do. I'm gonna go create notifyCardHolder, which exports instead of a function, an object with this function email on it.
[00:11:15] Same error, come back, and, oh the unusual spending thing, the actual thing under test doesn't exist yet, so that probably should exist, so let's go ahead and create that. And we'll just also export a vanilla function. Right now we're not trying to solve everything, we just want to see a logical failure.
[00:11:29] So if we can change the message here, that's good enough, red, and we change the message. Now it says, unsatisfied verification on test double. Test double JS picked up on what we were replacing, and it it, it named the test double, NotifyCardHolder. email. It wants to be called with 42 and high spending, but it was never invoked.
[00:11:46] That's the error message that we get. Alright now we can actually start to implement. So let's pull in FetchPayments. The high spending, notify cardholder, and then try to write the function. So we take in a user ID, we call fetchPayments, pass it a user ID and a callback. Given that callback's return value, we call determineHighSpending to get the spending, and then we call notifyCardholder.
[00:12:04] email, and we pass those things together. Now our hope is, we go green! And actually, we do go green. So there's our one passing test. That's all pretty cool. We wrote a test. Good job, everybody. That only took us 13 minutes. But we did it. Of course you might think wow, that was a lot of work just to write one test and it doesn't actually prove anything works.
[00:12:21] It just broke the problem down. And so I understand that it might feel not productive, but remember going slow is actually valuable. If you think back to TDE's benefits, first focus in, in my high anxiety world if I'm thinking about the entire problem all at once, it's very difficult for me to get started and understand where to work first, bottom up, top down.
[00:12:39] This is a nice way because now that we've solved that top level, we actually can break the problem down into three discrete elements, and very often in my actual day to day practice, I'll never go up and change that top level of code again, I'm really just reducing the problem as I go. Now imagine like how we broke down the top.
[00:12:55] We might break down fetch payments the same way, where it's a collaborator of something that gets months maybe a this month and last month function. They're pure functions, so they just need a simple unit test, not with test doubles at that bottom level. Because you, the goal here is to shake out as many leaf nodes in this tree of just pure functions as possible, not just scattered testables everywhere.
[00:13:15] And so it's a base case in the recursion of our functional tree of units. The payment API, meanwhile, is probably wrapping a third party API Writing a test isn't going to influence the design of that third party API because we don't own it. So I'd just write a wrapper there and probably not write a unit test for that thing.
[00:13:29] I'd let some integration test handle it for me. And as again, base case in our recursion. So that leaf node is now done. We can walk back up. So that helps you focus. Sense of progress. Remember, look at these errors that we got. We got couldn't find, make this module. Finally, we got like a logical failure.
[00:13:45] And then we went green. That was a pretty healthy sense of progress as we worked. As opposed to traditional TDD where we're like, Okay, so how the hell do I assert an email got sent? Or even though they exist, or how do I make the fake payment API? Oh and they haven't made an email API yet. I can't do this, now I got all this other work.
[00:14:01] That does not contribute to a sense of progress. Curing blank page syndrome I tend to ask like how questions too much, like how am I going to fetch payments, how am I going to do this, how am I going to do this, everything seems really impossible until I know how this helps me solve the problem by saying how is this top thing going to work, and then what does that top thing need to do the job, which are much more natural questions when you're designing, it's easy to answer what questions, harder to answer how questions without getting it all in your head.
[00:14:25] Slowing us down, another benefit. Again, most JavaScript I see looks like this. This is really tiny and hard to read, but this actually implements the same stuff and all in one big function that combines like 30 different responsibilities all in one place. Totally common, totally works. In fact, I wrote this in 10 minutes, roughly the same amount of time, and it actually works, and I can prove it.
[00:14:45] So naturally, my boss would love me, because I'd get the story done so quickly. But, of course, that's a temporary love, because what I'm really doing is creating this unmaintainable mess of anonymous functions. And that's just not a real sustainable way for us to build serious applications. So that's, again, what I mean when I say TDD is a soft skill.
[00:15:02] All of these benefits are non technical, but they pay off huge dividends in the form of less technical debt in the future. I've got a few minutes left. We're going to rattle through just some of the API Pass that example. To create a testdouble function, you can just call td. function. That makes it one called smile.
[00:15:19] To create one based on an object, you can pass it an array of names, and it'll come back with those. Or you can pass it an object that's a function bag and it'll know, okay, these are the two functions. I'll test double those two. Or you could pass it an instantiable function, pass it the constructor, and then it'll come back with an imaginary instance of that constructed function with well named test doubles.
[00:15:40] Like we saw with with a replace API here, we can have like a foo. js that's replaced. And then automatically, because it exports a function test double JS knows This is a testable function, but if it had exported an object like we saw, then that means that if I replace bar, it's smart enough to know, okay, so bar is an object and baz is one of the testables on that object.
[00:16:00] It also works with constructors. Another thing about testdouble. js that I think stands out is that it's very easy to debug. In a lot of cases test doubles are confusing, right? A lot of people don't understand them very well. And so it's useful to be able to call td. explain and pass in a test double and get information like the call count information about each of the calls against it, as well as a English language description of when it's been invoked, how it's been stubbed, and the interactions.
[00:16:24] And so debugging is a cinch. TD wins API it's a little bit more to it, we have this rehearsal idea here where you have one called drive, you pass 60, and if you pass 60, it returns dumpster fire but if you pass it 55, then it's undefined, but you can also do things as a series, so if you got this powerball lottery function, you can tell it return 5, and then 18, and then 4, and it'll do exactly that, and then it'll keep returning 4.
[00:16:49] You can also tell it times. The times option will only do it so many times, or on verify, it'll verify that it happens exactly two times. So here it'll stub nine twice, but then subsequent calls will be undefined. It's also one lineable, which I like because I like terse tests. So I can say, when td.
[00:17:05] function drive define it right there, 60, then return this, it'll return that whole test double for me, that'll just work so that's kinda cool. Also, it's intentionally hard to stub unconditionally what I don't want is if I've stubbed just drive with no args, drive 60 doesn't satisfy that, so it's undefined.
[00:17:21] But you have to call it with no args. You're writing an isolated unit test, so you're in control of how this thing works. And for whatever reason, C known and Jasmine Spies, they just are default really loosey goosey, and I don't think that's valuable. We also have this concept called matchers, like anything.
[00:17:35] So you can just say real quickly, Hey, drive anything, then return ambulance, drive 60, drive left, whatever. But if the args don't match up, then it returns undefined. It's not satisfied. Let's see, you can, it could be at any argument position, same kind of thing. We also have this option, ignore extra args. So if you do want a loosey goosey thing, then you can have additional args and it'll always return satisfy the stubbing, so to speak, we have an is a thing that matches on type.
[00:17:59] So is a number will match 60, but not left. We have a contains thing. Contains the string room, then returns speedboat. Drive room here. That contains room, so that matches. Other stuff, it doesn't. It also matches on arrays. So since room is one of the elements of this array, it returns speedboat.
[00:18:16] Works on objects as well. So here we say speed 60, return traffic light. So that one obviously doesn't match. This one does, it matches exactly. But if there's other junk in it too, it still matches. It also works with nested objects, so you can have a very sparse definition saying like you can imagine a huge API and you're just focused on one little thing, then you can have just that one little thing defined and it'll match and anything that's that you specified that doesn't match, that's enough to not satisfy the stubbing.
[00:18:42] We have a arg that where you can just pass in a lambda. So here I'm like, okay the speed is less than 55, then return truck. So 54 matches, but 60 does not satisfy the stubbing. Okay. Verify is, like I said, totally symmetrical. So everything we just said about when applies to verify. You see it typically like this.
[00:18:58] I create one called exhaust. I call it with a cloud, and then back in my test, I verify it was called with a cloud, and nothing happens. Because it satisfied the condition. If I call it differently, however, then it'll throw the verification error, saying it was actually called with this.
[00:19:12] I expected it to be called with this. We can also reset cause TD has a lot of global and local state stuff going on. So if I create a test double called bark and I call it, it'll return the thing, but if I call TD reset, all of my test doubles go back to a clean slate. Always make sure when you're using test double.
[00:19:29] js and some after each somewhere, just call reset once. And if you do that, you won't have any test pollution. Guaranteed. So yeah, give all this stuff a try. This kata was unusual spending. Test double dot js. That's where test double lives. And then teeny test is the test runner behind all this.
[00:19:45] Thanks a lot. This is a lot of content really quickly. You can find me on Twitter, Sorrels. I'd love to hear your feedback on this talk as well as on test double. js. It's still very new. Just recently 1. 0'd. So it is production ready, safe for work. I come from an agency called Test Double. We're a consultancy.
[00:20:01] We're focused on working alongside engineering teams. So if you're hiring senior developers and you'd like also to have people who are trying to level up everyone around them and testing and design and serious software development topics consider working with us. So you can just get ahold of me.
[00:20:14] I got stickers and business cards. I'd love to talk to you. And that's all she wrote. So thanks a lot, everybody.