Crafting Code Podcast

~/podcast

$ cd episodes/008-test-driven-development

~/podcast/episodes/008-test-driven-development $ ls -1a
. .. episode-summary.txt references.txt themes.txt transcript.txt get-mp3.sh
~/podcast/episodes/008-test-driven-development $ cat episode-summary.txt

TDD is one of the most powerful, professional practices we are aware of for designing code. It provides a consistent way to make sure you solve the problem at hand while naturally improving your code quality. This episode takes a deep dive into your hosts' thoughts on the practice.

~/podcast/episodes/008-test-driven-development $ cat references.txt ~/podcast/episodes/008-test-driven-development $ cat themes.txt ~/podcast/episodes/008-test-driven-development
$ cat transcript.txt

[00:00:00] Allan Stewart: Welcome to episode eight of the Crafting Code podcast, where we discuss the importance of doing the right thing at the right time with the right tools. I'm Allan Stewart. I'm a software architect. And recently I've been wondering about whether the one ring is sentient or if it just reflects Sauron's will in the minds of those near it. I'm Dave Adsit, CTO, and I have recently been

[00:00:23] Dave Adsit: wondering how to fulfill the responsibilities of a technology organization within a company

[00:00:29] Matt Baker: beyond just supporting product development. Hey, I'm Matt Baker, software architect. Lately, I've been thinking about app store review times and why they keep getting longer.

[00:00:40] Allan Stewart: Today's topic is test-driven development. And this is a topic that we have touched upon many times on this podcast already in our few episodes so far, but we feel like it's something so important that we wanted to take a little bit deeper dive and understand it better and really Why we are talking about it all the time, why we're promoting it. So to kick things off,

[00:01:05] Dave Adsit: what is test-driven development? Well, test-driven development is the process where you start with some form of automated test before you write code. And then the automated test helps you drive out the code that you actually need in your system. Basically, you start with an automated specification for the code, and then you fulfill the specification by writing the code that it matches.

[00:01:39] Matt Baker: Yeah, those are two big questions for me. The what is TDD? You can, I guess, just start with it's a test-driven development. It's kind of a design methodology. I think some people even refer to it as a test-driven design. And then for me, how do you do it? Also a pretty big question, but I guess you could just open the conversation with you write your tests, before you write the code your test is covering.

[00:02:01] Allan Stewart: But isn't that kind of backwards? I've definitely talked with people who feel like it is like a backwards way of developing because they don't know how to test it because they haven't written it. They haven't figured out what it is. So how do you get over that hurdle of figuring out how to put the test in front of the thing that you're trying to do?

[00:02:22] Matt Baker: Yeah, that's a good question. What comes to mind first for me is you, for me anyway, So I started thinking a lot about code as I started doing TDD. And one of the ways was instead of thinking about maybe the whole kit and caboodle of what I was going to write, I would start focusing more on how I might interact with the thing. You know, when I adopted that mindset, TDD became a little bit more natural for me. Like if it's an API I'm writing, I'll write a test. And in that test, I'll invoke my API and I'll have it do something. And it's one way to approach designing something, right? Where you first think about how the consumers of the thing you're building will interact with it. Prior to doing TDD, I would think more internally first, I think. I would think about the guts of the thing and then build my way out and expose what I had built via interfaces. And so it does inverse that a little bit, I think. And I'm sure that and a ton of other reasons contribute to why it does seem backwards, especially as you try it out the first few times. It definitely seems wonky.

[00:03:17] Dave Adsit: So in my experience, what I have found is that by writing the test first, I am forced to think about what the code is supposed to accomplish. If I write the code first, I can write a test afterwards that ensures that it does what it does. But I don't necessarily know if that was the right thing in the broader system. So if I sit down and say, I need code that will do the thing, whatever that is, I can describe doing the thing with a test. And then when the code does the thing, I know I'm done. If I start writing the code first, I can... I can write a test that ensures the code doesn't change, but it may not actually be doing the right thing first. Or may not be doing the right thing when I'm done with it. I think one of the cool things about test-driven development is, or TDD, test-driven design, whichever form you want to use. One of the cool things about TDD is that it lets you write just the code you need to solve the problem at hand. It also forces you to think a lot. Right. And it lets you think a lot more about what the code is supposed to be doing rather than all of the things that it could be doing.

[00:04:36] Allan Stewart: Yes. It's like a constraint that you put on your system or that you're putting on your code writing habits. For me, I know that sometimes I would write code and I would get an idea. It's like, okay, I'm thinking about the problem. I think about the problem for a little while and I think, all right, great. I've got a solution for the whole thing. And so I start writing a solution down. And then I'd also start interspersing some of these other concepts that are important to me, like writing clean code and keeping coupling low. And so I'd be changing things as I went. I'm trying to do the design of my class and solve the solution for the problem. And it's really easy to kind of get off track and say, oh, I was so worried about the design of the software from like an extensibility point or something like that. Because, you know, I was thinking too fast. I was thinking too far ahead of what we might need in the future that I lose track of what was needed right now. And so putting that constraint in place of, well, make this test pass is a great thing for me in my process, because that way I make that pass. A lot of, as far as, you know, the basics of doing test-driven development, I often heard it said that you follow the red green refactor pattern. So first you write a failing test that makes it go red because it's failing. And then you write the code that makes that test pass. And now you could just bounce straight from there back to now write the next failing test. But if you skip that refactoring step, then you miss out on some of that other software design things where you could switch gears and say, hey, we can improve the code. And for me, going in those individual steps has really helped constrain my problem solving. It constrains the way that I write the code. So first I think, what am I trying to accomplish? And nothing more. How do I? How, if I were the consumer of it, like Matt was saying, of an API or a class or a call into a database or whatever it is, how do I wish it would act? Write the test. Now I'm going to make it pass. Just only enough to make it pass. And then I'm going to look at what I've done so far. And especially after a few loops through this process of writing a test and making it pass, you pretty quickly can get to that point where you're like, you know, there's a better way. And now you've got all the tests. Because you could do the refactoring and in the refactoring step, everything should stay green. If you break anything, then you say, oops, nope. Start over that. If you're really disciplined about it, maybe you even have done like a get commit in the middle there. So you can reset hard and you say, nope, just kidding. Start over with that. Try again. It gives you this nice regimented way of making your software so that it meets the requirements, but also gives you still that opportunity to make it pass. To make it better, which goes to your point, I think, Matt, of designing software.

[00:07:33] Dave Adsit: I really love the refactor phase of the TDD cycle. It lets you introduce all of those things that you know will improve the code. You can reduce duplication, whatever it is that you need to do to improve the code. But you've got all of your specifications just right there in executable form. Everything that you had thought of up to that point, you know the code is going to keep. Because you've locked it in place with a vice. I think of it like in woodworking, you put clamps and vices and things on the project to keep it from moving while you're gluing or drilling or whatever. And the tests can do that same thing for you. Kevlin Henney talks about good unit tests or guts. Programming with guts, right? Good unit tests. And my experience is the best. The best way to get good unit tests is to write them before you write the code. I've tried to add tests after I've written code many times, and I cannot even count the number of times that I have missed one weird little subtle edge case or whatever that I wasn't considering when I wrote the tests. But I would have made sure to put in if I had been writing the test first. Some of those are the hardest bugs to find and clean up.

[00:08:58] Matt Baker: Yeah, one of the things that sticks out to me about TDD these days is when you make a change, or rather when you write some tests, write a test, write some code, write some more tests, write some more code, and you go through that for a while. You've got a couple hundred lines of code, let's say, and you go to make a semi-significant change in there. When you have those tests, when you make that change, you'll know right away if they broke your program, at least as your tests have expressed it. And that, you know, if we call that feedback for a minute. When it comes to writing software, at this point in my career, I just don't know of a better way to get that feedback loop in early and often all along as I'm writing the code, right? So from the first line of code that I write, it's under a test. And so I know, yes, it does, at least as good as my tests are. You know, it says it does what my tests express. And I think there are some other ways to get that. Like I've played around with REPL-driven development, and I think that you can capture a lot of the same spirit of what we're talking about. For one reason or another, it's typically TDD with me. But more and more, I think about it as a design discipline. You know, it's just a way I write code to keep my design as close to my spec, you know, at least as best as I understand it.

[00:10:10] Dave Adsit: You know, one of the things I've been thinking about a lot recently is how easy it is to design in your head. And I think part of that is because you can gloss over the tricky bits, right? Allan was talking about if you start by doing the design and then implement it. Implementing the code and then writing the test. I think that you get to gloss over all the tricky bits. And I'm reminded of one time Matt came up to me, and he had been working with a team. He was frustrated, and he's like, the devil's in the details. And it's software. It's all details. Yes. That's why we have to write code early and often when we're exploring a problem. And that is how the tests help us, right? Like the tests can drive us to a design that actually works as opposed to one we imagined would work. When we started with a whiteboard, my whiteboard still doesn't have a compile button on it.

[00:11:03] Allan Stewart: Not even for interviews.

[00:11:04] Dave Adsit: Not even for interviews.

[00:11:06] Matt Baker: Yeah. It's so easy when your design is underpinned by just an affirmative on all points of integration without verifying. It's great.

[00:11:18] Dave Adsit: It is. It's super easy. It makes you wonder why we ever tried to leave the ivory tower. Yeah. Yeah. Yeah. Maybe we didn't. I don't know.

[00:11:32] Allan Stewart: Well, so as far as design goes, and also what you were talking about before, Dave, about good unit tests, I feel like TDD helps you with that. When I first learned how to write unit tests, I was learning just kind of the basics of unit testing in general. And a lot of the code that I was testing was hard to test. There's a tendency in that space when you're testing after the fact that you want to test it. So whatever it says in the code, I'm going to test all of those things. And you make your tests overly specified. And that in turn makes them more brittle. Because it wasn't testing what you needed to have done. It was kind of like cementing in. This is the way that the last person did it, whether that was you or someone else. When you write the test first, you really can focus more on what is my objective here. And it doesn't matter. If some of the methods end up being private or if you split things out in different ways down the road. Because you've really specified what you were wanting to get out of it. Which in turn then also helps you create a better design for the software. A lot of the first things that I was learning how to unit test were extremely difficult. Because there was so much setup involved. Because these classes had thousands of lines. And they had UI code mixed up with business logic. Mixed up with presentation logic. Or controller logic. All of these different kinds of things were all hodgepodge together. And that's hard to test. But if you break it into these discrete steps. Individual pieces that each have a test with here's my setup. And here's what I expect to have happened. Then it's a lot easier to have effective tests. And just drives you toward better designed code. As far as a lot of the principles around like solid design principles. From the OO space. Or even functional design principles. Things like pure functions. They're a lot easier to test. And so if you write with that in mind. Then it's easier to stay on that path.

[00:13:37] Dave Adsit: Well and if you go all the way back to the fundamentals. You want high cohesion and low coupling. So you want each component to be highly cohesive. So everything in it is related to the one idea. It's not multiple ideas stuck together. It's one idea. And low coupling. So you can swap things in. And out. If you start by doing tests. If you TDD your way in. You are going to be pushed in a direction. Where you create more cohesive. Less coupled components. There's just no way around it. Because if you write the test first. You're going to think of ways to build things that interact. That don't require huge amounts of that setup. No one starts their TDD session by saying. I'm going to write a test with 200 lines of setup. To execute one method. And then 60 lines of verification. To make sure the right thing happened.

[00:14:36] Allan Stewart: So this kind of covers some of the reasons. Why we have been promoting TDD. It's a thing that we have often talked about on the podcast. And I think just kind of in broad strokes. You should practice TDD as a professional. Because it's a professional way of coding. Right? So if we go back to these concepts. Of the software. The software craftsmanship manifesto. Or even kind of our tagline for the podcast. About doing the right thing. At the right time with the right tools. Well TDD is sort of a tool. To help you do the right thing. It helps you make sure that. The code that you're writing is correct. It helps you make sure that it's. Accomplishing the goal that it needs to accomplish. Rather than getting off into gold plating territory. Where you're gone off the rails. And you're building something else. That wasn't actually what was asked for. Right? And it helps you make sure that you have high test coverage. So thinking about testing as. You know, an important principle in the industry. I've found that when I do test driven development, I very rarely need to. Worry about my code coverage metrics. Because they're always high because I always write tests first. If I'm going to add some new functionality, then I add a test that will be that new branch of logic. And so I don't have to worry about these weird edge cases because I always test them in. So that's why in my opinion, you should practice it as a professional. But why would a business want you to practice it? For me?

[00:16:05] Matt Baker: One of the things that jumps out real quick is momentum. I think. And I'm sure I'm super biased here because I, you know, I like TDD, but for me, it's always felt like it helps me get the project going at a reasonable pace. I think that like within maybe the first few days, you can make an argument that you'll cover more ground without TDD. And that's probably true. But for me, I like to. Do. do it from day one. And it helps me establish a cadence that I can maintain as the project grows. There's definitely an experience I'm sure a lot of people can relate to where if you start a code base and you don't cover it up with tests, as that thing grows, it becomes harder to manage. You change something in one place and then in a completely unexpected area, it breaks something else. And you start to find out how coupled and interdependent your system is. And as that continues to grow, it can really become problematic. And I found with TDD, it helps combat that. And so what I think that means to the business is as you want new features, as we're going into new markets, as you want to pivot, or just as you want to continue on your roadmap, we can kind of keep, I'll just say we can keep momentum, you know, and we keep moving forward and we're not, we're less likely to run into things like the big rewrite or, you know, where features that used to take a week or let's say a month now, are getting quoted at like a year, you know, and those things happen as these code bases grow without a test coverage or some way of keeping them under control. And so as a business, I think for your longevity and to maintain a steady pace of value delivery, it's a good way to ensure it

[00:17:36] Dave Adsit: technically. Yeah. If you have a code base with five features, it's really easy to manually test all five of those features each time you want to release that code. A year later, when you have 500 of those features. And some of them interact. If you don't have an automated way to run your tests of the entire code base, it's going to take you significantly longer to certify that the latest build does what it's supposed to do before you can release it. So you're just going to slow down your

[00:18:08] Matt Baker: release cadence unnecessarily, in my opinion. And it gets long, you know, to put some numbers to that. I've seen, I think the longest I've seen was a three month, like freeze QA to release. You know, and then that's maybe on the extreme side. And then in some of these other places, maybe a more average one, a couple of weeks to QA release and prepare it when you're, when you're working in some of those places, it can get wild.

[00:18:33] Allan Stewart: Yeah. I've got similar experiences. And that's one of the things that I really like about TDD as a practice, because the output, it's like sort of a side effect is that you get this regression suite, which we know from other learnings about testing, you know, the anecdotes that you were just saying. About, you know, being able to regression test and know that your system works correctly. They're super valuable, but you're not even trying to really do that when you TDD, you're not even thinking about that. You're just thinking about, Hey, how do I solve today's problem? And as a side effect, you get the tests, but meanwhile, it opens you up to doing all this design work so that you're designing better code, better classes, better integrations between things, et cetera.

[00:19:18] Dave Adsit: One of the things that I've experienced when I've been, when I've been using TDD as one of my techniques or practices on a code base is that it helps me identify the different components of the code. And when something starts to get complex and hard to test, it can help me see that there's another idea trying to get out of that thing. Right? So I'm writing a class and it's getting big and the setup's getting hard. And then while I'm doing the refactoring, I might realize, Oh, this is complex because there's a lot of stuff that I need to do. code components, right? We need reusable code. And first of all, don't design for reusability, design for usability first. And if you happen to reuse it, what a bonus, but it's not a primary goal or shouldn't be. I have found that most code designed for reusability isn't usable in the first place, but code designed for usability can often be reused, which is great, right? So you can find those small components that you can use over and over. I think that there's no coincidence there

[00:20:53] Allan Stewart: that a lot of the code that is designed to be reusable is designed to be frameworks. Yeah. I

[00:20:59] Matt Baker: like to say like the reusable components in your code will come knock on your door. You know, they'll say, Hey, I'm over here. And by the time you've written the same thing seven or eight times you go, Hmm, you know, this, there seems to be some repetition here. And then maybe you have, maybe not, but maybe you have enough of a sample set to say, I can see the pattern here, you know, and then you extract because it'll be benefit you then like right then, you know, because the next thing you add, you get benefit from, but when you front load that stuff, you just delay all the value and what you deliver oftentimes is so like generalized

[00:21:32] Dave Adsit: that it's useless. Well, and that goes back to the idea that designing the big system is more interesting and more fun because you get to defer all of those pesky details of the actual problem You can trick yourself into believing that you are quite busy and doing productive things without

[00:21:52] Matt Baker: actually ever delivering anything of value. Imagine like showing up to play a basketball game. And when you're getting there, declare that you're actually going to golf. And then like you put on the side, like, that's what it feels like. Like you show you're like, you know, I'm going to make my own rules and my own goals here. And then I'm going to celebrate when I accomplish those. So when I get the hole in one over here, you know, it's just like, it's weird. It's weird to me that it's, it's a total disconnect from, I guess it's just a meta on the problem you're there to solve. Like what's the root of this problem and how does it, how is it similar with other things like it? And then how do I code for that? So in the future, when the next thing comes along, I'm ready. It'll be great. It's golden. Right. And it's like, I get the intention and I've fallen victim to it myself. I've screwed up projects doing this very thing. And it's from those lessons that I'm like, don't do it. Just, just solve the immediate problem, solve it over you'll see the patterns where you need to generalize and it'll make sense for your

[00:22:50] Dave Adsit: strategy and what your business needs. Well, and sometimes it's, it's like more directly related. Like you show up to play basketball and you're like, well, first I'm going to make some shoes. Cause I need some really good shoes. In fact, I'm not even going to make shoes. I'm going to make a sewing machine that can stitch leather and rubber, because I know I'm going to need that to make the shoes so that I can play basketball. And then we're going to crush it. Right. And code that looks like,

[00:23:16] Matt Baker: like, let me reflect over this and find the method in vocation. You know, you're in danger

[00:23:23] Dave Adsit: when you start talking like that. That goes back to don't write your own ORM ever. If you think this is a situation where I need my own ORM, step back, take a couple of days and think about the

[00:23:38] Matt Baker: choices you've made. If you have to write an ORM, please write a parser for your query language. So it's not all strings. Because that'll save us when we eventually have to rewrite it and we can lean on static code analysis.

[00:23:52] Dave Adsit: Throw it away. So we've talked quite a bit about unit testing, not directly by name, but we've talked a lot about like testing things in the small or whatever, but there's other things that we need to test if we're going to TDD a code base. And sometimes it's referred to as the testing pyramid or whatever. One of the ones that discussion of ORM makes me think about is like, what do you do? When you need to integrate with stuff outside your system? What kind of things do you tackle there?

[00:24:24] Matt Baker: What a question. Yeah. You have so many choices here, right? All the different things that you depend on and that you interact with. My short answer on this these days is don't test these things unless they're volatile, unless like you need to ensure every time you release that they're doing what you expect them to be doing. And maybe that's everything, but there are some services that You can rely more on than others, at least me anyway, that's been my experience, but

[00:24:50] Dave Adsit: yeah, be cautious with these. I have found that I've narrowed my definition of integration test significantly over the years. Now I think of an integration test as a test that verifies my integration with a third party system, whether that's a database that I'm also responsible for, or an API I'm calling. I like to write a test that, that validates my interaction with that thing so that I can run that every time I release to make sure that the database has stood up correctly. The API hasn't changed for the service, the interface into the, I don't know, the library or whatever is still consistent with my expectation. Everything is changing all the time to your point, Matt. Everything is changing all the time in software development. Every dependency you have is always being updated. And so if you're still using the dependency correctly, some tests will really help. And I call those my integration tests. I usually write those on an adapter or an adapter type layer. That's my code that takes that other code and turns it into concepts that make sense in my domain. Yeah. Yeah. I like that. Maybe

[00:26:11] Matt Baker: to give some examples that come to my mind when I think about dependencies you might want to test don't. When I have maybe a database that my service depends on, I won't test every transaction against that database often. I might just test that I can reach it and then maybe a onesie twosie depending. But I've found in practice that sometimes I'll get a little lazy on covering every interaction with that database as far as is that database there and can I connect with it? So I don't know. I may not cover every test or every piece of code that reaches into that database, but I will. Like if it's an external dependency, if it's a third-party API, then I'll typically cover every

[00:26:51] Dave Adsit: single one of those transactions. With databases, what I like to test is if I have an entity in my system and I put it in the database and I ask the database to give it back to me, did I get the same thing? That is my basic test. And it's usually like one test that creates an object, saves it, loads it, compares what was loaded to what was saved. Right. Because the interaction that I care about with the database is you persisted this entity properly.

[00:27:25] Allan Stewart: Yes. Yep. Agreed. Yeah. And I like what both of you said. The word my came up a lot. Test my code, right? That's really what you're after. You don't want to test all the other code, right? You don't need to test all of the frameworks and databases and all these things that you're using. You want to test the code that what you're doing works. And so bringing that back to test-driven development, I really like using that at that layer, right? So to use Dave's example of, can I store this entity and then get it back? That's a pretty easy test to write first and say, okay, this is my expectation. I'm going to make this little object and I'm going to call save. And then I'm going to call load and I'm going to compare the two. It's a really easy test to write. And then it keeps it so that And then maybe also delete and update, but you get those, those real basic fundamentals down and you don't accidentally pollute it with other business logic or something else that was happening. There's less of a temptation to throw in stored procedures or something else to make the database do those things because you say, no, all I want the database to do is hold this thing

[00:28:46] Dave Adsit: give it back to me. That is all. Which, you know, that speaks to one of the important aspects of testing. When I hear people say it's really hard to test the database. I'm like, it's not hard to test it the way I do it, but I also don't ask my database to do things that belong in

[00:29:04] Allan Stewart: application logic. Well, and you're not trying to test the internals of the database. Like do the triggers fire when triggers should fire? Like, no, that's somebody else's job. Always because

[00:29:14] Dave Adsit: there should never be triggers. So this, this leads to one of the ways that I think about software design and database design and system design, right? Is that a data store should not be doing logic because typically that's going to be an expensive component in your architecture and scaling it up as your demand goes up is non-trivial. But if you have all of your logic in the appropriate place, which is the application tier, then you can buy a bunch of commodity servers. And scale wide and you've minimized the operations that are happening in your database. And maybe this is just something that has been a concept that I've needed to adopt because of the types of systems I've worked on. I've done a lot of websites that have had a lot of different amounts of traffic on them, like high scale and low scale. And in every case, it's been cheaper to scale my application tier, my web server tier, than it has been my database tier. given the choice, that's where I will optimize. You may have a different context. I don't know.

[00:30:22] Matt Baker: I haven't seen it yet. One of the things you said, that it can be really hard to test a database, or some people might say it's really hard to test a database. And a good way to get around that is to write a test first, like we are talking about, right? You're not going to write tests that are really hard to execute if you write them first, right? They're going to be, I want the database to do this thing, and then you'll go implement to pass that test. In this case, like if you're testing a repository or some, you know, some transaction with the database, right? Just enough to pass that test and then go on. And you can avoid some of the trouble that, you know, Dave's talking about where you let things creep into your data or into your database that shouldn't be there, like triggers and stored procedures that can, I've seen some stored procedures that I thought, oh yeah, that's a clever solution for a problem. And they've made sense, but that's been the exception for me. Oftentimes they're a signal of trouble. As they, especially as they grow anything over, you know, thousands of lines is, that's a lot to see when you open up a stored procedure.

[00:31:28] Allan Stewart: Yeah. I think that applies as you move up the testing pyramid too. Generally speaking, I feel like the higher you go in the testing pyramid, the harder the things are to test. They tend to be more brittle and you want to have fewer tests, but you can use test-driven development to make that easier. So if you're going to need a little bit more of a test, you can do a UI test. Don't try to look at the UI and say, how do I test all the things? How do I test all the colors and the CSS classes and the hover states and all? Like, no, don't, don't do that. Just like you shouldn't do that at a unit testing level. Don't try to get the entire implementation, figure out what's important and just test that little bit that makes it so that your website works or your app user interface or whatever it is that that works. And then you can move on.

[00:32:16] Matt Baker: One good practical signal I think you can use here is if you, this is maybe somewhat heretical to say, depending on who you are, but more and more, I've been stepping away from mocks. And as I step away from that, I don't know, my test suites get smaller, but still as effective. So I think that's a positive signal for me. But when you're mocking, if you, if you start to assert in your tests, every bit of your implementation. So if you say like, for instance, should write to log file, and then you go, write to log file, and then you say, should call the database repository dependency with these values. You can take that too far. I think, I think there's value in it, but I think you can stretch it to the point where you almost have, I've heard, I've heard it called like a tautological test where you've, you've stated the implementation and then gone and implemented it. And I don't know, lately I've been pulling back from that and doing more, less testing with mocks. And Allan, that's where I started thinking about, you know, just what's necessary to assert the functionality you need is there. And when I focus on that, I find myself doing those kinds of internal implementation specs, less and less.

[00:33:24] Allan Stewart: How do you get started with test driven development? It's another skill that you have to develop, right? Like you need to learn some of the basics of testing and using a testing framework, whatever is applicable for your coding environment. And then you just, you have to practice because it can really feel different, right? It's, it's a different way to think about, you know, what's the best way to do it. And so I think that's where I really like using code katas as a way to practice as a way to get started on something like test driven development, because if you jump right in and you say, okay, I'm going to do this in my production code. First thing, this is my first attempt. And I have all the complexity of the design that was there, which oftentimes is not designed well for tests. Like we mentioned before, you have all the complexity of the frameworks and things. And if I am in that world of doing it in legacy code, where you do have all these other things surrounding you, this bigger context, then I've usually tried to insert it in a very small place. Don't test the thing that needs the dependency injection. Don't test the thing that needs the database. Just make this one little class off on the side and test drive that because that way you're working on new code in order to learn the practice of test-driven development, rather than trying to start off by saying, what is the hardest way to do this? Because then you're just going to get frustrated and give up on it right away.

[00:35:18] Dave Adsit: Completely agree with that. The hardest code to test is code that was written over time without tests. You can really get discouraged if you try to jump in and unit test code like that. I've heard people say, how would we test drive code like this? Test driving would never result in code this bad. One of the teams that I worked on years and years ago, they used to be able to summon me by saying, this code is untestable. You would immediately hear my chair rolling across the floor as I came to rescue the code and the developer because I was confident and competent at testing. And a lot of the people on the team were less so. Also, I have perhaps brash confidence about myself.

[00:36:31] Allan Stewart: changes to an existing code base. And some of the previous developers seem to be kind of terrified by this prospect. They're like, what are you doing? You're just, you're changing everything. But one of the things that you've said about this that I think applies to this discussion is that when you get good at a thing, you start having the confidence and the competency to be able to go in and make code do what you want it to do. So if you know how to do testing, if you know how to do TDD, because you've practiced and you've learned, you can get to the point where you understand how you want the thing to be and why you want to change it. And then the things that seem from the other perspective, very drastic change. You don't have to fear it because you know, well, I can just put it back. I have all the tools in place. I've written tests that prove my new thing. And I've got things like source control. So if I really mess it up, I'll put it back. Hopefully you have other tools. I've got tools that help with this whole process too, like continuous integration. There's a pipeline that helps me test all my code and get it out to production in an automated way. Then you can experiment more and be bold in trying those things without worrying that the change that you're making is going to just collapse everything. Because I've worked in that code too. Some code bases feel like a spider's web where you touch any one point and it will affect the whole rest of the code base.

[00:37:59] Matt Baker: When I think about places where you can get started with TDD, if you have any size of code base, one thing that sticks out to me, go into a function that, or a portion of a function anyway, that doesn't have any side effects or doesn't call out to anything externally, maybe say that's like a pure piece of code, you know, and you can extract that. It should be trivial to do. And then you can put that function that you extracted it to under test. And you can just start kind of testing or start working on your testing chops. And when you write that test, you can approach it dead simple and just say, you know, here's the function I'm going to call and then state in your code, here's what I expect to return. Oftentimes I just create a variable called expected, and then I'll assign it to whatever I expect to come out of this function I'm about to invoke. And then I create another variable called actual. In actual, I store the result of the function under test. And then I just assert that they're equal. And most testing libraries have some, you know, method of assignment. And then I just assert that they're or feature work to go to all this stuff. Don't do that. It's just a little bit at a time and you'd be surprised how fast it spreads.

[00:39:34] Dave Adsit: I've had very similar experiences. I've actually found, you know, probably for the last 15 years, I've been doing web application development with various frameworks. And a lot of them didn't lend themselves well to testing a while ago. They're getting better and better, but still, even the most modern ones that I use, there's still some edges where they didn't make it easy to test, right? So you might end up with a whole bunch of code in a controller action or some kind of thing that responds to an HTTP request. And you could take a lot of that code, as you were saying, Matt, and push it back to another class so that that front layer, that UI layer, isn't really doing anything except invoking this other class. And now you can go and start to write tests on that class. Right? You can say, I'm going to... You put a test around this that asserts that it does the behavior that I expected. And maybe I'm going to pull out some of the dependencies and start injecting them so that I can do those tests with mocks because I like to use mocks. I mean, your results may vary. But once you've started to do that, now you can add a test for the new functionality that you want or start to gather together various things. Like I spent a lot of time taking web forms code, ASP.NET web forms, and putting it into Model-View presenter patterns so that we could test drive the presenters or at least add unit tests to the presenters. And then the Model-View controller frameworks came out and got better and better. And so I don't do so much of that anymore. But I still like to have that layer, that top layer usually is describing a feature or a function that a user cares about. And that makes it easy to wrap your head around what tests do you need? This is accomplishing something. HTTP came in and a response goes out. Now I know what to test.

[00:41:36] Matt Baker: So when you start approaching a code base that hasn't had any tests in it, or maybe it has tests, but it wasn't written with TDD, especially if you're working on that code base with other people, it can be a little bit interesting, right? To try and do TDD when the people around you are not doing TDD. Because often as you get into it, you find that it really does impact the way you write code. An obvious example of that is dependency injection. So like what Dave was talking about, you need dependency injection in order to use mocks. So you can inject your dependencies into what you're testing in order to assert their interactions with those things. If your class doesn't have that, it's pretty hard to do that style of testing, right? And so if someone working next to you doesn't write their classes that way, and eventually you have to end up working on that code, there can be some interesting points of chafing, I guess. And I was curious if either of you had seen other things emerge when you're trying to TDD in a code base that others aren't.

[00:42:38] Dave Adsit: So I've actually got kind of a weird story about TDD, and maybe it's not as weird as I hope. I was told that I was going to be starting on a project as a new developer on the project. And everybody I talked to in the company, I was like, oh, you're going to go on that project? I'm sorry, that project's weird. The people who wrote that project originally just did so many weird things with the code. It's so strange and hard to work with. Everything's topsy-turvy and backwards. And I was like, well, we'll see about that. I'm going to go and refactor my way to glory, I guess. And I get into the code base and I start exploring it. And the developer who had been working on it had left the company. So there was nobody to really talk to, about it. And I'm digging in and I'm looking at this and I'm looking at that and I'm seeing where the problems are. And it turns out that the developers had used an ORM and there didn't seem to be any weird patterns that I was unfamiliar with. There were things like injecting dependencies and repositories were called out as a concept. And there's a whole bunch of patterns that were really familiar to me. And I was like, this isn't weird at all. What are they talking about? And I'm like, this was written for testing and there's no testing. I wonder. And I go and look and sure enough, in the source control, there was a test suite for the entire code base that had not been maintained. And in fact, had been intentionally excluded because nobody understood it or what to do with it. And I tried to revive the test code for about a day and it was hopeless because the code had moved too far and too many of the dependency injection and separation of concerns and cohesion and low coupling concepts had been abandoned. So I guess the point there is it's really hard to see a test base wilt and die. And it's easy for a team to kill a test base in a very quick fashion. It's hard when you're the only person who's writing tests.

[00:44:46] Allan Stewart: I think there's kind of two layers to me of this question. There's the layer of, are other people doing tests at all? Because if they're not, that's always going to make it harder. It's going to be harder to maintain tests and so forth. But then there's also the layer of, if they are doing testing, does it matter if the other people are doing TDD or not? And I think to a degree it does. To your point, Matt, sometimes when you do test-driven development, it changes the way you design your code. And so you start structuring things in a different way. Like the code, the database that Dave was just talking about, was written for testing. And that's why it seems weird to people who are not familiar with that. And so I think generally speaking, if other people are doing tests after, like I've worked in projects where there was a requirement that there had to be tests. And I did TDD and others did tests after. And for me, that worked out great because then I seemed like a rockstar developer that could just get stuff done really quick because I got the things done and it worked correctly. And I got all my tests done in short order, which was mysterious to the other people who would write their code and then struggle to figure out how to test it. And so they weren't getting done. But in other cases, if you're divergent because you're testing and others aren't, or you're divergent because the way that you're writing your tests is changing the shape of your code. And so you've got apples and oranges code. It's going to be harder. It's definitely more difficult to keep that up as you're going. But I think that there's still some benefits there. A lot of the things don't have to be used. Like dependency injection makes testing a lot easier and it might be harder to test something that doesn't have that. But if the team isn't using that principle, there's often ways that you can subtly tweak things. So maybe there's not constructor-based injection, but you add your own method-based injection specifically for the test. Or you write a wrapper around your class that news up all the things that are going to be needed when it gets used by everybody else. But then there's kind of like this tested core piece that used injection. I mean, I think you could even throw the tests away afterwards. If nobody's going to maintain those tests, you could still use testing to drive your design and get the code where you want it to be. And knowing that nobody's going to maintain them, you just throw the tests away. Afterward, at least you've got arguably better code as a result. Just so long as that better code isn't so dissimilar from everybody else's code that they are going to come in and revert it or make any other of those kinds of changes that just makes it incompatible. Oil and water kind of code.

[00:47:41] Dave Adsit: One of the things that I would say about being the only person on the team who is test driving the code is you need to have buy-in from the team. and you've done everything you can to change your team and it's not working, I think the only thing you can do at that point is change your team. You can change the way your company works or you can change the company that you work for. And if you are a test-driven developer, if you're test-infected and you use CI and CD, you are on your way to being a 10x developer. I've heard people say there's no such thing as a 10x developer and I don't believe that's true. What I do believe is that there's variability in the amount of productivity of individual developers and some can write code faster and some write code slower. A 10x developer is one who creates a system that's 10 times as effective. And that's going to involve a suite of automated regression tests. It's going to involve writing just the right code at the right time. It's going to involve things like CI, CD, pair programming, mob programming, etc. All of those things make the system significantly more effective. And if you are on that pathway, then you should stay on that pathway even if it means leaving behind people who don't want to walk it with you.

[00:49:37] Matt Baker: I agree with that. I think if this is interesting to you and you see value there and you just absolutely can't get your current employer to let you run with it, and you are fortunate enough to be able to change your job, do it. Absolutely. If you get that chance, I think you'll learn a ton. And you'll definitely, if you're interested, I'm sure you'll just, well, I don't know, maybe you won't like it, but at least I hope you take the chance if you get it.

[00:50:06] Dave Adsit: I can tell you the first time I was introduced to test-driven development, I didn't get it. I was like, why? Seems like we're writing at least twice as much code, maybe more, just to get simple functions and features done. Right? Right. I'm going to write five tests for some of the methods. What are you even talking about? That's so much wasted code. Turns out code isn't really waste. Like it doesn't consume actual resources. So the concept of waste is tricky, but I didn't adopt it the first time it was introduced to me. And then I went and worked at another company where we didn't do test-driven development. In fact, we edited code live on production servers, but that's a story for another day. And then I, went to a company after that, where we test drove everything, including trying to test drive out our UI, which was super tricky because this was in the infancy of the web frameworks. The hot debate on the team was whether we should use prototype or I don't even remember the other library because none of them are relevant after jQuery came out. But we test drove everything and I learned a lot. And it, it bent my brain in ways that made it hard to put back.

[00:51:24] Allan Stewart: This takes me back to concepts that come out of the craftsmanship manifesto, right? You want to work with a community of professionals. You want to have productive partnerships that includes within your team. We've talked before about code being a mechanism for communication. It's for communicating with humans more than the machine, right? The machine doesn't need most of what we do to make, good, clean, maintainable code, but the humans need it, including our future selves. And so I think about that in terms of, can you have a productive partnership, not just with your customer, but with the rest of your team, you know, as far as what we say on, on this podcast as our motto, going back to it again, you know, doing the right thing at the right time with the right tools. If the rest of your team isn't on board, if they're going to undo all of your work, maybe that's not the right tool or not the right time for you. plain old C objects, C sharp objects in this case, Java, they call them POJOs because they're Java objects. And sometimes people call them data transfer objects or DTOs because they're just, it's just a carrier for a bunch of data and it doesn't do anything. So don't test it if it doesn't do anything. Boundary code is another place I feel like just be careful what you test there. Dave talked about extracting code, your business logic out of a web controller, right? The controller that actually receives an HTTP request and returns an HTTP response. That's good enough. That's enough work for that to do. Don't try to test that because it's super hard because you're right up at the edge of the framework. You're at the boundary where you're going from someone else's code into your code. Super hard to test. And so I often I'll avoid testing that layer and move all of my stuff out one class or one level of abstraction so that then I can be testing there because you don't want to be tied to these things that make it really hard to test. And you don't want to be caught up in the dogma that everything has to be tested all the time. TDD is the only way ever. So what are some places for you, Dave, Matt, what are places that you don't test or don't test drive?

[00:54:14] Dave Adsit: Well, the ones that you mentioned are definitely on the list. I thought a lot about not testing UI, meaning don't test the CSS or don't test the form properties or don't test that the XAML is exactly right. Whatever your UI layer is, I don't find much value in testing it because when I do a visual inspection, I can tell if it's right. Now, on the other hand, I think that we have gotten to the point where web frameworks and web browsers have advanced sufficiently that your in-browser application is actually a separate application that takes your API as a dependency that should be behind an adapter and it should be tested independently. But I wouldn't ever test that the value of the text color of the CSS class is the right blue. You look at it and it's right and you move on. So the test, the code that you're testing is manual, but very, very trivial. I guess this is only kind of an answer to the question, but if you ever find yourself testing your mocking library, you have made a mistake in your testing and you should unwind some of those tests. Don't test other people's code. Only test your interaction with other people's code. I don't test the framework. If the framework doesn't work, all I can do is file a bug report and hope. I have to code around that problem anyway. What do they say in Pragmatic Programmer? Select isn't broken. So I don't write tests for those things. The one exception to that is when I'm doing exploratory tests, when I don't understand a library or a framework or a toolkit or whatever, I'll write some tests because it's a very easy way to execute some code and learn some stuff. But I probably don't commit those tests and I certainly

[00:56:04] Matt Baker: don't add them to my integration suite. Yeah, it's a good question that when you asked it, I had a lot of good answers for it, but as I started thinking about it, I realized it's actually kind of complicated for me. Like on the UI, I don't test, like Dave was talking about, I won't test my CSS colors or my HTML structure. And I often won't even test my data bindings. Like if you have some JavaScript settings, some stuff, I won't test the stuff that's intended to be set, get set. But that's only true because I make sure that those data bindings don't have any functions or any sort of expression other than just a literal that says this DOM element equals this value or this CS whatever. But I'll take those, the functions that generate the data that's to be bound, I'll extract those. And if they're interesting, I'll test those. So it's JavaScript, it's front-end UI code, but it's a type of front-end, right? Where it has some of that logic in the client side. And so it has to be client-side logic it's basically a client-side application. That's right. Right? Yeah, absolutely.

[00:57:17] Dave Adsit: If there's logic out there, which there often is, that becomes a separate application and you should test the heart of it. But to Allan's point, don't necessarily test the boundaries,

[00:57:31] Matt Baker: like the bindings. Yeah. No reason to test those, or I don't test those bindings anyway. I also won't test every third part, like an API For instance, I won't explicitly write contract tests for every third-party service I use. I'll rely on integration tests. The different components are depending on those services, and I hope that those are covered. But I won't write specs for integration libraries on those all the time. Sometimes I'll write it for, like, can I authenticate against this thing? And then can I execute one of the gets or something like that? Sure. So there's definitely nuance to external dependencies for me, too.

[00:58:10] Allan Stewart: Yeah, I like that. That idea of separating out your business logic, I think, can't be overstated. So in this case, we were talking about it in terms of view, right? So inside of view, you might have view logic, and view logic can be tested. But the display, the bindings, that stuff, very rarely do you want to test that. You know, earlier we talked about it with HTTP controllers. I think it also applies to things like the database, or really almost any boundary where from your code to somewhere else. There's something there where it's like your code can be tested much more easily because you own it. If you just put that abstraction in place that separates your stuff from theirs, that makes testing really a lot better. I also think about the context in which you're doing testing. So another place where I don't do test-driven development is if I'm not going to maintain the code afterward. If I'm just doing one time I was writing some JavaScript stuff, throwing it together, because what I was really doing was just learning how to use D3 and do graphing. Or I'm writing some other kind of maybe like an ad hoc function that I'm not going to use more than once. I'm going to run it a bunch of times to maybe generate some data or to transform data from one thing to another. And I'm just visually inspecting it. That's basically my test is, is this working? Did the output look the way that I wanted it to? And that was enough. And so I don't test those things. And at the end, I'm just going to throw them away. And so that's a place where test-driven development doesn't make a lot of sense. You could even take that out a layer. Like if you go outside of the product development mindset, where you're saying, this is our product and we're going to keep it and maintain it. That's a very different space than if you're doing project work. So like, if you're working at a project shop where every day or every week or every month, you have a new customer and they ask you to come in and write some code to solve a specific problem. And you're going to write that code and do that thing and give it to them. And at the end you're done, they own the code. You're never going to touch it again. Then maybe it doesn't make sense to do testing there. Personally, I think there's an argument around like, you know, the veracity of

[01:01:07] Matt Baker: core. And as I get out to that imperative shell, there's still tests there, but the concentration

[01:01:12] Dave Adsit: was just not as much typically. Definitely. As you were talking, I was thinking about Dan North talks about a project that he didn't test drive, not specifically with automated tests, because the developer was sitting right next to the user and they were working together. The user was executing the software as the developers writing it. And they would make changes and make changes and it would work or it wouldn't work. And then after a month or two, they would throw it away and start over. And the context there was small applications that have a limited shelf life and a very, very tight feedback loop. So I could see that being a case where you might not need the feedback loop of the testing. If the expert... The expert user is just sitting right there with you executing it. Like how many times have we not been able to figure out a problem? And so we run the code in debug mode. It's kind of like running the code in debug mode all the time, which of course doesn't scale to tens of thousands of concurrent users on a web application, but it might scale to one person sitting right next to

[01:02:26] Matt Baker: you in a specific business environment. One final thought for me on TDD as you, if you're adopting it, maybe for the first time, if it's new to you, it's going to be weird. And it'll be weird for a little while. It takes a little bit of getting used to and you won't see the benefits. I don't think day one, you'll have the tests and that'll be great. And I guess we can call that in itself a benefit, but the benefit to your code and the way you think about problems and the way you structure your code will change if you keep writing TDD. And those are some of the benefits I'm talking about. They take a little while to come, but once it starts to click, if it's going to click for you, it's worth holding out, for a little bit. So it will slow you down a little bit at first, unfortunately, which is I think why you don't want to adopt it wholesale right away. Just take it slow and give it a little bit of time and it might start giving you some value. Well, with that, I think we'll

[01:03:22] Allan Stewart: wrap up this episode. Test driven development, it's an important practice, one that we believe in very much, especially in the vein of it is the best way that we've figured out how to write code. Maybe we'll find something better. And if so, we will try to adopt that. But in the meantime, we will recommend to all of our listeners that you join up with a community of professionals by attending a software crafter group or meet up near you. Here in Utah, the Utah SC group at utahsc.org meets the first Wednesday of each month in Draper, Utah. We often do test driven development as one of the things that we practice as part of that meetup. So maybe we will see you there.

~/podcast/episodes/008-test-driven-development $ cat ../../copyright.txt

Copyright © 2026 - Crafting Code Podcast