RailsConf 2022

Your TDD Treasure Map

Your TDD Treasure Map

by Christopher "Aji" Slater

In his presentation "Your TDD Treasure Map" at RailsConf 2022, Christopher "Aji" Slater focuses on the importance of test-driven development (TDD) and offers strategies for applying it effectively. Slater begins by acknowledging that while testing is crucial in software development, many developers, especially those new to the field, struggle to implement it. Using the metaphor of a treasure map, he illustrates how flowcharts can help navigate the complexities of testing and writing code.

Key Points Discussed:

  • Significance of Testing: Slater emphasizes that robust automated tests can prevent regressions, support refactoring, provide documentation, and guide design decisions.
  • Treasure Map Analogy: He introduces the idea of a treasure map as a visual representation of code flow, explaining that it can serve as a guide for developers to identify decision points in their code, which can lead to different outcomes, much like a flowchart leading to treasure or peril.
  • Flowcharts in Testing: Slater demonstrates how to create a flowchart based on acceptance criteria and then use it to generate test cases, reflecting the control flow in the application. He presents a practical example of a Rails controller handling user updates, showcasing how to derive test cases from the map.
  • Contextual Testing: The discussion includes how to handle different contexts within the testing framework and how variations in parameters or decision branches can affect the flow of execution.
  • Incremental Development: Slater highlights the iterative nature of TDD, encouraging developers to begin with simple tests and gradually add complexity based on user stories and acceptance criteria.
  • Encapsulation of Logic: He advises on encapsulating complex logic in service objects, discussing how this can simplify the flowchart and testing by abstracting responsibilities away from controllers.
  • Final Thoughts: The presentation illustrates that there are no strict rules for applying the treasure map technique; instead, it is a flexible approach that can be adapted to various development frameworks.

Main Takeaways:

  • TDD is a valuable approach for ensuring software integrity and can be made approachable through visual mapping techniques.
  • Using flowcharts to visualize decision paths enables developers to better understand how to structure tests for their code.
  • The process of writing tests should be seen as non-linear; it evolves through iterative exploration and adjustment of the codebase.
  • Practical application examples are crucial to grounding testing strategies in real-world development scenarios.

Through this engaging talk, Slater equips both novice and seasoned developers with insights and techniques for navigating the sometimes treacherous waters of TDD, ultimately steering them towards a more robust test suite.

00:00:12.599 Hello, hello! Welcome, everybody. Thank you so much for coming out. I mean, ahoy!
00:00:21.660 Shouting into the microphone is always great. My name is Aji, that's a hard 'J'. I've been a professional developer since 2016 but if you count Geocities, I've been putting code on the web since 1996.
00:00:36.000 Anyone in the room not yet born in 1996? A couple of you? That's awesome! I'm so glad you're here. I do regret asking that question a little bit, though.
00:00:49.020 I never really know what to say during these intros, so how about this: in fourth grade, I got the lead in the school play 'Mice and Mozart' where we told the life story of Wolfgang Amadeus Mozart through the eyes of a family of mice that lived in his walls. I played Mozart at nine, and I've been trying to relive that glory ever since.
00:01:13.439 Let's assume we're on the same page about something: I think everyone here agrees that testing is important and that test-driven development (TDD) can be massively beneficial. You're at a software conference with a community that overwhelmingly appreciates automated testing, and you've come to listen to your TDD treasure map, which might just imply that TDD is a kind of treasure.
00:01:41.280 So, I'm going to skip the arguments that try to convince you of the benefits of robust automated tests that verify your code is working properly, defend against regressions, support refactoring, act as living documentation, and provide design guidance. Although I didn't really understand the nuances at the time, that was certainly the feeling they tried to instill in us at my coding boot camp. However, I graduated without ever writing a test, despite feeling the benefits of a good test suite. Plus, it seemed like a paradox: the thing that verifies the code I will have written before I write it.
00:02:01.320 Sure, let me just jump into my time machine and take a look at that pull request I’m going to put up tomorrow. Testing first can be difficult — that's true for seasoned sailors but even more so for those newer to this career when it's a challenge to reason about what code to write at all. How was I going to make it out of this mess?
00:02:25.500 Like Archimedes in the bathtub or Newton and gravity, we need a compelling yet apocryphal story of discovery to accompany it. Scratching my head over this problem, I wandered over to my bookshelf filled with all my career-related books. There's Sandy Metz's 'Practical Object-Oriented Design in Ruby', and Atul Gawande's 'The Checklist Manifesto', wonderful tomes that have set me on the right path when I've needed them before.
00:02:58.680 Wait! That's Robert Louis Stevenson's 'Treasure Island'. How did that end up on my shelf? I'm the child of a librarian, and this is so embarrassing! Miss shelving is happening in my own home. Oh, but then it dawned on me: of course this is here because treasure maps!
00:03:32.400 Think about it: a treasure map is a flowchart. One of the core strengths of a flowchart is its branching logic, allowing us to map out decisions that lead to different outcomes. Captain Flint only wants the happy path that ends in doubloons. If we fill out the map, we put a decision branch here at the edge of this dense jungle. Without the map in our hands, we'd be lost in '404 Not Found'. One way leads to treasure, and the other to an encampment of rival pirates and '409 Conflict'. A decision here — East means treasure, but West means being eaten by crocodiles. Just like that, the whole crew is gone.
00:04:05.220 Not only is a flowchart useful for retracing your steps to your long-buried, ill-begotten gains, but it's also one of the most helpful artifacts for understanding the control flow of a system and provides invaluable documentation for new team members. It serves as a kind of visual pseudo-code. Think about all the low-code and no-code solutions that are built on flowcharts as a UI because they're so simple to understand yet potentially powerful.
00:04:40.440 We can leverage that same strength as a shared language to build understanding between technical and non-technical team members and stakeholders, and to get buy-in from the business in ways that we’ve never been able to with code. But the usefulness of this tool won’t stop at the code's edge; we're going to learn how to directly apply it to writing tests and eventually code.
00:05:07.200 Consider yourselves hornswaggled and snared into being my crew aboard the Good Ship Capybara, en route to the promised land where testing before you leave harbor can be smooth sailing. Before this half hour is over, we'll raid the ship of knowledge, and you'll have new techniques of your own to boot.
00:05:42.240 I do want to apologize for that accent to everyone watching who is a 17th-century Caribbean pirate — sorry, privateer. A chart can relate fairly directly to code, serving to mimic the control flow of an app. However, how it relates to testing might not be as obvious. We'll use a pretty common situation of a Rails controller action to demonstrate how we can take a handful of acceptance criteria, turn that into a flowchart, and then rig it into test code and application code.
00:06:05.040 A couple of quick caveats and definitions to simplify our discussion: when I say 'feature', I mean any unit of work — be it a whole feature, a bug fix, partial implementation, or whatever. I'll stick to saying 'feature' for brevity. Also, when I talk about test suite rigging, I mean the describe context and it blocks that make up the tests — those lines with the descriptive strings and the levels of indentation that show how they relate to one another. That's what I’m calling the rigging.
00:06:45.300 And although this process will work with any testing library, Ruby or not, my examples will be in RSpec, because as we all know, it is a pirate's favorite testing framework. The app our team is building is called Pirate Cove, where freelance freebooters can list their available skills and ratings for captains looking to round out a crew for upcoming misdeeds — I mean, adventures.
00:07:16.139 Our app has users; we call them pirates. Pirates have profile pages; it's standard Rails resource routing with the usual CRUD actions. This afternoon, we’re pairing — don’t worry, I’ll drive — and we’ve picked up a ticket with a new user story: as a pirate, I should be able to fill out the form on my profile page and submit it to change my information.
00:07:40.380 The acceptance criteria, phrased differently, is that a user can update a pirate's information from the profile page. There are actually two eventual acceptance criteria or requirements on this ticket, but we're going to start with just the first and pick up the second as we go. So, we’re going to begin with the action we know is going to take place: typically, a user acting on the system, which here means our pirate submitting form data.
00:08:12.639 Standard flowchart symbols have start and endpoints as rounded shapes, so we'll start with that core action at the top center of our workspace. When that gets submitted, an action occurs on the back end; we’re going to save the data. How do we resolve? Well, we show the updated information with a redirect to show. This alone could represent the happy path of our feature.
00:09:02.640 We know it will get a little more complex, but at least it's enough to write the matching rigging. Our rigging begins with a user action, some back end work, and feedback for the user — beginning, middle, end.
00:09:37.679 Now at the top of an RSpec file, we get something like this to start, but everything we’re doing is going to be under 'patch update'. So this is the last time we’ll see this — to say, the slides are getting a bit too cluttered.
00:10:01.740 Right away, I can see two outcomes that we expect will happen after form data is submitted. By Rails convention, we’re redirecting to the pirate show page, and the only way that the pirate show page is going to have the proper information is if we also update the database to reflect the changed data.
00:10:54.180 This doesn’t mean that every bubble in the flowchart needs to be tested, and we'll see that coming up; but in this case, we’re testing more than just the very end, but also any action that changes something outside of our current test subject. A side effect — our test subject is the Pirates controller. This makes a change in the database way outside the controller; that’s a result we want to guarantee will happen.
00:11:13.280 So we’ll protect it with a test. If anyone ever comes along and makes a change that affects that outcome, even if the redirect still happens, we’ll have feedback if the expected result has been affected. That's our tests preventing regressions.
00:12:04.500 Now, the other piece to our test rigging are context blocks. The 'it' blocks are going to match up to the end results — the effects of the action. The context will correlate to the setups that exist, found by following each individual path through the chart. Right now, we’ve only got one path through the flowchart. It might make sense to wrap those two 'it' blocks in a context block; I don’t think we gain anything by doing that since that’s the only context that exists.
00:12:57.120 But that is already wrapped in that 'patch update' describe block. Personal preference, but I’d leave it off for now. What else? This indicates that, by being only one context, our setup for these two tests will be the same. Each individual path through the chart will have a different state when the action is performed. So far, we’ve focused on one action that ideally keeps a single inciting action.
00:13:37.800 In this ideal state, when you're focused on testing just one action, the only way to take different paths through the chart is with a different beginning state or different parameters to that action. So we’re still here, and we kind of know that it’s not going to stay this simple. This is a position you might find yourself in pretty often. You know more is looming, but it hasn't actually shown up just yet.
00:14:11.120 That’s not complicated; I’d rather start moving forward on something I’m sure of — naive or not — rather than speculate and end up down in some irrelevant rabbit hole. We’ve got concrete steps to take, so let’s move forward on that.
00:14:41.760 If nothing goes wrong, this is essentially what needs to happen. If we write these tests, we’ll have a North Star to guide us as we sail along, and if the seas get choppier and these tests fail, we should probably rethink our approach.
00:15:05.880 Now, the new tests will be preventing our own regressions. The next developer to work with your code could very well be yourself, even just an hour later.
00:15:30.300 Should we write some tests? Okay, before that — I'm also not here to introduce or debate the different methodologies for writing individual tests. That's another talk or workshop, and they mostly boil down to some variation of 'given, when, then'.
00:16:07.200 What might the test code for this portion of our rigging look like? Let's start with the setup: we know we need a pirate whose information is going to be updated, and whose ID will be part of the URL based on Rails RESTful conventions. The action we already said will trigger when the user submits the form data.
00:16:39.540 So if we use RSpec's request syntax, it might look something like this. Eventually, our expectation is that it should redirect. Here we are with test code for both 'it' blocks from before: it redirects to the user's show page and updates the database.
00:17:02.700 Now we’ve got some wind in our sails; we’ve understood our flowchart as a context and still with just one path. Our given is the matching setup, the action that begins our flowchart has become the 'when' — that's the action taken in our test cases.
00:17:32.160 We’ve identified the two end states: one for the HTTP response and one for the database. We’ve captured them as 'then' in the expectations. As I build out a system, I'm trying to uncover moments where we might be making assumptions that aren’t as guaranteed as we’re treating them.
00:18:01.680 So I'm asking myself, what here is built on a lie? What could go wrong? My eye is drawn to this: these params are sent in by a user — you know, users. So do I, and I don't trust them. They always find interesting ways to stress the system.
00:18:30.800 In reality, though, that’s a gift. It really is, and I believe that, but right now it’s a problem. So far, our tests are assuming everything is going to go smoothly. We need to handle what happens when the params don't pass validation.
00:19:02.760 Let’s fold up the test code like we're in our editor and add this params protection. What would params that don't pass validation change about the flow of this chart? That change will never make it to the database, and because that can fail, we need to add to the flowchart before the persistence a check for validity.
00:19:35.820 We’ll notate this fork in the road with a diamond, because in flowchart land, which is part of Diagramopolis, a diamond represents a decision — something in the setup or action that is affecting the outcome, which decides the branch of the flow to continue down.
00:20:57.420 Now we truly have more than one path through the feature and with it another context that needs testing. Let’s trace the two paths so we can see what we’re dealing with. If a path is a context, we’re able to tell that one context is different from the other. The nodes in the flowchart that correspond to existing specs can’t be reached by that new path — they form an entirely different context.
00:21:47.580 Let’s reflect that in the rigging. But how can we know what that context involves? It’s not as simple as an endpoint; there isn’t one node in the flowchart that equates to context, but many along a path. So let’s ask ourselves: what given setup or initial input will cause the decision to divert to the right, where we see the two 'it' blocks that we already have?
00:22:19.260 Valid params go right, and the opposite — invalid params — causes the flow to go left, resulting in the 'it' block based on Rails conventions that renders the edit page again and sends the object to the view with those validation errors attached.
00:23:01.200 So let's act on these tests in the flowchart. What do we want to do next? Personally, I'm not going to feel good about these tests until I see them fail. We haven't written any code for them yet, so if they pass, there's definitely something fishy going on.
00:23:52.980 Great news — the test failed! That means we can write some code to make them pass. We’re going to keep this as simple as possible, but let’s think about the code we'll need to follow the flowchart and satisfy the tests. We’ll need to know which pirate; if the update works, we’ll redirect, and if not, we’ll render edit — nothing fancy here, just direct from Rails generators, maybe condensed a little for the slides.
00:24:44.880 How did our tests fare? Nice! Good work, everybody. Let’s just take a moment to reflect on how we got here. We used the ticket requirements to make a simple flowchart that met the user story — the happy path — and from that flowchart came our test rigging.
00:25:36.480 We wrote some tests and uncovered a failure state, which led us to expand on our flowchart and rigging. From there, we used our understanding of the system at play to write the application code.
00:26:17.880 This process isn’t going to be linear. Most of the time, we won’t have written the entire flowchart with everything in scope then move on to the entire rigging, then the test code, then the app code. We’ll bounce around acting on the next bit of information that we're confident about, uncovering assumptions as we go, while adding to our framework in the process.
00:26:34.800 Remember that there is a second acceptance criteria. Number one: we can update a pirate's profile info. But number two says that only authorized pirates can perform updates. Before I check for validity in the same way that we added the valid step before persistence, we have a new step that can interrupt the process before reaching the end states we had charted before.
00:27:07.200 This leads to another path through the chart. For now, ignore the implementation of even having a logged-in user. I’ve only got 11 and a half more minutes with you, but another path should make us think about another context.
00:27:35.640 Using the same technique we did before, we can wrap the expectations based on when their paths diverge. The first time the paths diverge, they go in two different directions: green to the left, blue and orange to the right. This tells us that there will be two contexts, and it’s the decision where they branch that tells us what that context is.
00:28:12.480 Authorize? Yes — that’s green. Since the other paths share the state of a pirate who has permissions to change this data, the context about being authorized will wrap them both.
00:28:37.680 We’ve done this exercise before, and I’m bringing it up again because I want to throw a metaphorical match in our powder keg. What makes someone authorized is not so cut and dry — authorized: yes or no.
00:29:20.760 It’s perfectly fine for the flow chart here because at the controller level, that's the flow of this action. But to be authorized, the logged-in pirate is either the pirate being changed or a pirate with an admin role. What was once three paths through the chart is now five.
00:29:50.760 If we reflect that in the test cases, we go from four to seven. That’s 57 more tests, and I like tests, but conceptually, this doesn’t feel too much different from what we already had. We added this section, but it makes the same decision: authorized — yes or no. It's just a more complicated process.
00:30:34.500 Maybe you’re already standing where I’m leading you, or maybe you’re on the way, but if this box was opaque and all we saw was authorized — yes or no — then the rest of the paths would have the same information in the same flow. Does the Pirate controller really need to know how to tell if a pirate can make the change, or does it just need to know whether to make them walk the plank or not?
00:31:19.740 It’s the latter, right? Let’s encapsulate that logic in an object. Look at this — Robert Louis Stevenson and Sandy Metz back together after all this time! Because we can test the authorization logic over in this object’s tests, we can even call it a service object if we want.
00:31:52.860 It doesn’t have to be part of our controller test at all. We can mock the call to the service object within the test and force it to say yes or no as part of our setup. And suddenly, we’re back with just authorized — yes or no.
00:32:29.880 The next time you're looking to encapsulate some logic into a service object, look for parts of your flowchart where those paths diverge, do something self-contained, but then converge back together like we did here.
00:33:03.720 If we think about it, we already have similar nodes in this flowchart, don’t we? Right there, this isn’t a single thing; this is Active Record taking our params hash, doing something with that, traversing to a database, which is a whole different program. Just updating a resource can you imagine the flowchart on that?
00:34:01.920 Active Record just handles it. So we can put a single box in our flowchart in the same way that we zoomed in to get to an authorization service class. We also understand the zooming we could do behind the 'persist' node. We can also zoom out from our controller.
00:34:22.560 Until this point, we’ve been looking at a single action, and everything else happens like dominoes without our influence. This is a flowchart for ordering a book from Amazon — it’s very high level, really zoomed out. It’s all the way to the user; everything is abstracted away. They’re not thinking about HTML, servers, databases, or any of it.
00:35:05.820 And yet, this is an incredibly useful diagram, even for testing and even for writing code. Just like our happy path tests were our North Star while building, something like this can be the North Star for an entire business or development team.
00:35:39.780 They can ask themselves, can a user still order a book, or does this help a user order a book? Probably not, Amazon anymore, because nowhere on here do I see shoot a founder into space. I also don't see let him come back either, but we did that.
00:36:21.240 Each of these colored blocks represents an action by a user — sign in, add to cart. Each one of them could easily be its own controller action if it were Rails, each with its own flowchart as intricate as, but probably more so than, the one that we’ve just built. But at this level — can a user order a book? — those details aren’t important.
00:37:16.800 Still, each branch or flow will need a test, maybe at this level a context, maybe not — again, personal preference — but I’d see five or six tests there. Eventually, every action reached by the path of a test should be covered from start to end.
00:37:50.640 I hope that helps you see that there aren't hard and fast rules on how to do a treasure map technique, but here are some ideas and advice on how you might apply it in your own work. I literally do this in my work. This is a commit message I wrote last week.
00:38:43.200 That’s right, I put the flowchart in the commit message. If you want your commit messages to bring all the boys to the yard, the tool I use to make this plain text flowchart is ASCII Flow.
00:39:21.780 That's askiflo.com. Take these ideas, use them, throw away what isn’t working, and add on where it falls short.
00:39:59.160 If you do add on or come up with something cool with it, please tell me on Twitter, Instagram, GitHub — any of these things. I'm around, and I would love to know how you remix this or adapt it to other frameworks and languages.
00:40:49.140 This thing is open source, right? Because we might be here under the auspices of land-loving Rails rather than ocean waves, I can objectively see whether your features are crafted in Ruby, Emerald, carved from elm, adorned with jade and pearl, or are crested in Rust, or if this Elixir flies truthfully across the seas all the way from Java to Ceylon.
00:41:23.760 Let me be crystal clear with the final goal: to reach Davey Jones's locker and bash a hundred-foot python before I give up my test-driven development.
00:41:59.820 Now that you've seen the ample scholarship of this technique, I hope your acceptance criteria are always clear, your test suite never scuttles your deployment, and the cloud never takes the wind out of your sails so that the Good Ship Capybara can see you home.
00:42:37.560 Thank you.