Test-Driven Development (TDD)

Summarized using AI

TBC

Ben Orenstein • March 15, 2015 • Bath, UK

In this engaging live coding session presented by Ben Orenstein at BathRuby 2015, the theme revolves around programming fundamentals and the importance of code refactoring for maintaining cleanliness and structure in software design.

Ben, a self-described higher education dropout and a moderately skilled programmer, invites audience interaction throughout his talk, aiming for a collaborative atmosphere. The goal is not merely to demonstrate coding but to engage the audience, encouraging them to provide feedback, ask questions, and actively participate in the coding process.

Key points discussed during the session include:

- Live Coding Interaction: Ben emphasizes the value of discussion and back-and-forth interactions during coding sessions, likening the experience to pair programming where he drives the code and the audience navigates.

- Refactoring with the Single Responsibility Principle (SRP): The demonstration focuses on a class responsible for sending email invitations, where Ben showcases the necessity of adhering to SRP. He identifies the dilution of responsibilities in his initial implementation and proposes refactoring to separate the parsing logic into its own class.

- Practical Coding Examples: Ben provides clear examples using Vim to code and demonstrates real-time test results. He shows how to use test-driven development (TDD) effectively to iteratively develop and refine the code, emphasizing the importance of running tests frequently to ensure the code behaves as expected.

- Dependency Injection: He explains the advantages of dependency injection for managing complexity in code. Through this approach, classes do not create their dependencies but receive them as parameters, enhancing the clarity of code flow and simplifying testing.

- Managing Complexity: The session highlights that complexity arises from mixing different concerns in code. By following SRP and managing dependencies wisely, developers can maintain cleaner and more maintainable codebases.

The presentation wraps up with Ben reinforcing the importance of SRP and dependency management, urging attendees to keep these principles in mind when building applications. He concludes with a catchy jingle summarizing the message, thanking the audience for their participation and engagement throughout the session.

TBC
Ben Orenstein • March 15, 2015 • Bath, UK

By, Ben Orenstein

Help us caption & translate this video!

http://amara.org/v/GZe3/

BathRuby 2015

00:00:24.760 Thank you! Good morning, everyone. It's so nice to see all of you. This is literally my favorite thing to do, to stand in front of a bunch of brilliant, talented, beautiful Rubyists and talk to you about programming. This is my happy place.
00:00:31.199 I'm also really excited to be sharing this stage with amazing speakers. I feel like this lineup is ridiculously good and a bit intimidating, which is kind of awesome. Also, I'm super excited to notice that 33% of our speakers are Thoughtbot employees, which I think is pretty cool.
00:00:43.239 If I can just recruit one more before the end of the day, that will make it 50%. So, Linda, think about it! Sandy, you know, Tom, anybody, please talk to me at the break.
00:01:00.239 So, like Linda, I am a higher education dropout and a mediocre programmer. But I'm trying to get better; I'm focusing on the fundamentals. This talk is going to be a live coding session where we'll write code together. I will refactor it in front of you, but the goal is not just to reach the end.
00:01:14.400 It's not for you to just sit there and listen; the point is interaction. Imagine that we are all pairing: I will be driving and you will be navigating.
00:01:26.520 Please talk, please give feedback, please ask questions—I want to hear from you. My benchmark for success in a talk is how much back-and-forth we have. I want to have conversations, to stop and discuss different aspects or side topics. I love that; it's my favorite thing. So let's get started and look at some code!
00:01:36.439 This is, by the way, live coding in the Bath. Can I have a monitor? There it is! Excellent. We are using Vim, by the way. Any Vim fans in the audience? Yes? Excellent! Any Emacs fans in the audience? Whoa, wow! Okay, I mean, I kind of like Emacs too; it’s nice.
00:02:08.080 So, please read this code. Can you bump the font up a little? I can increase the font just a bit. Here's something I've realized: this is how my test results look at this font size, and it actually works. But if I increase the font even more, it doesn't work. Let me prove it to you.
00:02:40.000 Here's a failure. And when my tests fail, it looks like this. You can see the failure at the bottom. Let's do that again. I’ll remove the failure when the tests pass—it goes away. But now I’m going to add the failure back in and see what happens when I bump my font size one more time.
00:03:04.159 Unfortunately, this is the maximum font size. I’m really sorry; I didn’t anticipate this exactly. So please bear with me. My apologies for the difficulty in reading. But please squint and read this code—I’ll scroll down for you once you get the gist of it.
00:03:36.239 Okay. A quick overview: this is a class that sends invitations. It takes a CSV of people we want to invite and a message to send to each of them. For each of the recipients, which we will get to, we create a mailer, send a mail to their name and email, and include the message. Then we deliver it.
00:03:48.480 The way we get the recipients is by parsing the incoming CSV and turning it into a hash. So how do we feel about this code? Any comments or initial impressions?
00:04:05.200 What's that? Ouch. Was that your response? Often, the code I write makes people say 'ouch.' Yeah, what's 'ouch-y' about it? What if my parsing fails while I'm delivering? One piece of feedback was: what happens if my parsing of recipients fails while I’m in the middle of delivering? I might send like 50 emails, hit a parse error, and then who knows what happened to the others?
00:04:31.720 That's good feedback. So the person who commented is likely someone who has sent a lot of emails, probably through background jobs with retriable jobs. Any other thoughts on this code?
00:04:51.199 You are about 25 minutes ahead of where we are exactly! His feedback was that he expected the CSV to be an injected dependency and thought it was strange to send in a CSV full of names and then parse it there. Is that a fair summary? Absolutely.
00:05:05.960 To me, this is code that I would probably write as a first pass. My general approach to solving any problem is to first make it work—make the test pass—and then make it beautiful later. I would look at this and say, 'Okay, we take in a CSV full of unparsed data, we parse it, and then we send an email for each of those items.'
00:05:50.080 This sounds very much like we have multiple jobs in this class; there are multiple responsibilities here. We all know that we want to follow the Single Responsibility Principle (SRP). I shouldn’t say we all know; you might consider following SRP, as it can often make your code better if your classes have a focused job.
00:06:08.000 Often, I find myself in this position where I muddy up a class with another responsibility. I say, 'Okay, now it's time to extract it.' So let's extract this code. We’re going to do the extract class refactoring right now, and I’ll try to do it as cleanly as possible. We're focusing on the fundamentals, so it’s going to get a bit more complicated soon, but let's start with some simple steps.
00:06:32.480 What's the first step in the extract class refactoring? Anybody? Name the class! That's reasonable. Yeah, the parsing of the recipients. What does that mean for the first step? Set it as an instance variable instead.
00:06:44.160 For me, I like to think about my first step and write the code I wish I had. This is a classic TDD trick, and we are going to be doing TDD, by the way. I have tests, and this is what it looks like when the tests run—it passes and then goes away. You don’t need to see anything other than success. When they fail, the error message stays visible.
00:07:00.440 Notice it takes me two keystrokes and almost no time to run my tests. That’s important in TDD. If you have to leave the editor to run your tests, you're kind of doing TDD, but not really. People who do that will abandon TDD, claiming it's too slow. In reality, TDD isn't the problem; it's the way tests are being run that causes the slowdown.
00:07:44.480 So let's start writing the code we wish we had. I wish we had a parser. Whenever I have to parse something, I want the parsing to be on its own; it's a task that can easily be separated into a class. So, what do we do now? Run the tests? Correct answer. What happens? Uninitialized constant Parser. That’s fair; I just made it up on the spot.
00:08:02.039 Let's make a parser. So, the next task is to create the class. Just the class? Just the class. I’m going to take it one step further. I’m going to create the spec first—I know I'm going to have a parser, and I want targeted specs for it.
00:08:13.520 I run this, and I get the same failure. This is actually great, as I'm extracting a class from an existing class. The unit tests for the Invitations class are now pretty much integration tests, testing both the Invitation class and the new Parser class I’m creating. When I'm writing code, I often start with integration tests—let those fail, and then I start writing unit tests to watch them pass.
00:08:36.440 We'll jump back and forth between the tests for these different files, ensuring that the messages in them stay consistent. So we have a test for our parser. Let's run that—oh no, we get the uninitialized constant parser error again. Let's split the screen vertically to work on it.
00:09:01.600 That’s how I usually work, pretty much with a low resolution. Now that we've created the class, let's go back to the Invitations class and rerun the test for it. And those tests pass. So, the next step is to write the code we wish we had. Now that we have this parser, I wish I could just ask the parser for the recipients directly.
00:09:39.720 So let’s run our tests again, and see what we get. There’s an undefined method error for recipients on the parser. Take a second to read these errors. I've done this talk a couple of times so I might read them faster than you. Now we need to drive out the behavior of the recipients method on our new parser class.
00:10:25.600 We are in the midst of extracting a class as part of our refactoring process, but we’re also going to move down one level of abstraction and push down the stack into the move method refactoring. We essentially want to move the contents of this recipients method into the parser class.
00:11:07.440 Here's the tricky aspect: there’s a difference between moving a public method and moving a private method. Because we are moving a private method, we only have tests that indirectly test that behavior. I write direct tests for my public methods, but I do not test private methods directly; instead, they get tested implicitly through the public methods.
00:11:56.480 You’re okay with that, right? Alright, let's move forward. Here’s what I’m going to do: I’m going to copy and paste. I’m going to use the tests for the deliver method in Invitations to drive out the behavior of the private method we’re pulling into our new parser class.
00:12:25.760 Let's check the tests; this should be about delivering. Cool, it sends emails to all the invites. I first set up the mailer, I create a CSV, and we expect the mailer to receive two method calls for each user in our CSV.
00:12:52.520 Now that we’re extracting a parser, this has become a component test, testing multiple classes at once. I’m going to copy this test over and adjust it so that we have something working that tests our recipients method directly.
00:13:29.680 So, I'll grab this code. I use visual mode, which is a total smell, but my screen's too small. Let’s modify it; rather than sending emails for invitations, we will check the result by passing a CSV into the parser and calling the recipients method, expecting it to return an array of hashes.
00:14:06.360 Let’s say this should look like: name, user one, email, [email protected]; and then user two, [email protected]. Again, just to recap, this is a CSV string representing name/email pairs. We ask the parser to parse it from the string and expect the result to be an array of hashes. Let's run our test!
00:14:50.520 We have a failure. We received an argument error indicating a wrong number of arguments. The method needs to accept a CSV and return the value. We'll fix that and then ensure the method handles the received CSV input.
00:15:40.520 So I’ll assign the method to take that one thing in and then just ignore it for now. I want the test's message to change. I'm intentionally moving in small steps. Now we can define the recipients method for the parser. This will fail because the method currently has no body.
00:16:01.520 Currently the test fails, saying it received nil instead of the expected output. Let’s uncomment the body, run the test again, and see what error we get. This can be confusing, but we don’t have CSV assigned. If we fix that, the test should pass.
00:16:55.920 Now we’ve extracted the class and completed the move method for extracting responsibilities. We’re making steady progress, and we can check the parser and invitations tests to ensure they both pass.
00:17:18.480 Before we move on, how are you feeling? Any questions, comments, or concerns? Great! Let's keep going and use the parser correctly in our invitations. We should inject the parser to make it work smoother.
00:17:38.640 Let’s get rid of the CSV. The mailer doesn't happen anymore, but now we are checking if the messages return properly. Okay, let's modify our invitations class to return a parser which will make things seamless. Now let's ensure everything runs smoothly!
00:18:25.520 We have just inverted control! Instead of having the Invitations class build its dependencies, we build the parser in the controller and pass that down. This makes the flow clearer and simpler, allowing us to inject the dependencies needed for the instructions ahead.
00:19:01.360 As I mentioned before, this has pros and cons to it. While it can become a bit tricky for people to grasp, this method allows our code to be cleaner and makes the tests easier to understand. Testing becomes simplified because we can focus on one piece of logic without digging through other integrations.
00:19:42.480 Now that we've set up the parser, we can easily specify dependencies. If we switch to an external service or require some time-based data, we can stub those as well, keeping our tests deterministic. This is reminiscent of libraries like VCR, which record HTTP interactions and streamline the process of swapping dependencies.
00:20:30.480 The major takeaway from today should be this: if you want to implement SRP, manage your dependencies smartly to keep your code clean, and well-structured! Complexity often arises when mixing concerns without proper control levels. Let's keep this principle in mind when building efficient applications, and I sincerely thank you for being here today.
00:21:12.640 As we reach the end of this session, I have one last point to share—this is a jingle I crafted to remind you of our main principle: if you want to follow SRP, don't manage your dependencies! Thank you all for being a fantastic audience!
Explore all talks recorded at BathRuby 2015
+2