RubyConf 2021

Clean RSpec: A Workshop on Ruby Testing Craftsmanship

Clean RSpec: A Workshop on Ruby Testing Craftsmanship

by Jesse Spevack

The video titled "Clean RSpec: A Workshop on Ruby Testing Craftsmanship" presented by Jesse Spevack at RubyConf 2021 focuses on improving the maintainability and readability of tests within the RSpec framework used in Ruby programming. The workshop aims to equip participants with essential skills for writing clear and maintainable test suites.

Key Points Discussed:

  • Introduction to RSpec and Code Smells: Jesse starts by advocating for comprehensible test files and discusses common 'code smells' found in RSpec tests. The objective is to ensure that tests are straightforward for new developers and team members.
  • Importance of Testing: Testing verifies that the code meets business requirements, catches bugs early, and serves as documentation for code functionality. Jesse emphasizes the dual nature of tests, contrasting unit tests (specific portions of logic) with integration tests (interactions of multiple components).
  • Three-Phase Test Pattern: Jesse outlines an efficient test structure known as Arrange, Act, Assert (AAA). This involves setting up dependencies (Arrange), executing the function (Act), and checking the outcomes (Assert).
  • Test Readability and Clarity: The workshop stresses the significance of writing tests for the reader rather than the writer. Techniques include using descriptive names, avoiding mysterious subjects, and organizing code to enhance clarity. Jesse provides examples demonstrating how to refactor complex tests into more understandable formats.
  • Test Doubles: The definition and appropriate use of test doubles are discussed. Jesse advises against stubbing methods of the object under test to avoid brittle tests that do not accurately reflect actual behavior. Instead, he suggests using mocks and proper dependency injection to maintain test integrity.

Significant Examples:

  • The Gilded Rose Kata: Jesse refers to this well-known coding reference as a framework for discussing best practices in RSpec, encouraging participants to refactor code examples collaboratively during the workshop.
  • Real-world Scenarios: Jesse shares personal anecdotes about the challenges of working with unclear RSpec files, reinforcing the need for improved test clarity.

Takeaways:

  • Adopting a structured approach helps in writing more maintainable tests.
  • Emphasizing readability when writing tests leads to a greater understanding for both current and future developers.
  • Using test doubles judiciously is critical for maintaining accurate tests that reflect actual code behavior.

Overall, the workshop provides valuable insights into crafting clean, efficient, and understandable RSpec tests, bridging the gap between initial coding and effective testing practices.

00:00:10.320 So, a few more people than I expected, but that's okay. We'll roll with this.
00:00:15.759 If you haven't already done this, head to the GitHub repository at github.com/jesse-speedvac/clean_rspec, fork and clone the repo, and if you can, please complete the welcome survey. This will help me understand who's here and what you're looking for in this presentation.
00:00:29.840 Also, if you want to get bonus points, you can grab a copy of the bingo board located in the getting started section. The idea here is that a workshop of this size is actually quite difficult to navigate, and the real tricky thing is understanding the level of each participant.
00:00:48.399 I might pitch this material way too high or way too low, but hopefully it will be just right. I want everyone to have lots of different options in this context. So if you're an RSpec wizard, grab that bingo board and perhaps this will be more entertaining for you.
00:01:06.000 I understand this is a prime spot; there are many awesome talks happening right now. If you need to get up and go to a different talk, I will take it very personally. I have a camera set up, and if you could tweet or something like, 'Hey, I had a great time at your talk but am heading to a better one,' that would be helpful.
00:01:24.240 Okay, I'm going to switch slides, so if you don't have the repository, make sure that the person sitting next to you has it so you can get it from them. So, I’ll introduce myself now. My name is Jesse Spevack, and I use he/him pronouns. I'm located in Denver, Colorado. These are my twin five-year-olds, Eddie and Audrey.
00:01:52.880 Raise your hand if you are local to Colorado. Wow, a lot of Coloradans here; that's awesome! Currently, I work at a company called Stripe, where we work on the economic infrastructure of the Internet. I wish I could tell you more about Stripe, but this is day two of my third week there, so maybe in a few months, you can ask me about it. I'm very excited about this!
00:02:18.319 Before Stripe, I worked at a company called Ibotta. Are there any Ibotta people here? Okay, cool! I see one person up front. Before I got into coding and the tech world, I came from a non-traditional background. I went through a program called Turing, located in Denver. Raise your hand if you're a Turing alum here.
00:02:40.879 Great! At the time I joined Turing in 2016, there was a proliferation of code schools with a wide array of quality. I would certainly say that Turing is on the high-end quality spectrum. One thing that makes it so high-quality is its mission, which is to bring folks from diverse, non-traditional backgrounds into fulfilling technical careers. I love my Turing community and think it's had a huge impact on this Ruby conference and the Ruby community in general.
00:03:06.560 Before I continue, I also want to take a moment to thank a few people. First, a big thank you to the RubyConf planning team for putting this event on. If you see anyone in a red shirt who is volunteering, please thank them! I also want to express my gratitude to the folks at Confreaks who are handling the video and audio. I appreciate your hard work, and finally, thank you all for coming and using this prime time at RubyConf to explore RSpec with me.
00:03:44.159 Now, raise your hand if you've ever heard of the Gilded Rose kata or a code kata. Let me ask a more general question: raise your hand if you have heard of a code kata before. Great! I'm supposed to remind you—don't be afraid of the mics. If you need to ask a question, please use the mic. So, the idea of a code kata is an exercise designed to learn a specific coding principle. There's a famous code kata called the Gilded Rose kata, which is noted for a couple of reasons.
00:04:14.400 For one, it has an origin story connected to World of Warcraft. Raise your hand if you've ever played World of Warcraft. Some folks are familiar with this game, which debuted in 2004 and became quite popular. Although I was never addicted to it, this kata incorporates concepts from a specific element of World of Warcraft called Gilded Rose. It has gained fame largely due to a talk given by Sandy Metz a few years ago called 'All the Little Things.'
00:04:51.840 Raise your hand if you've seen this talk from 2014. Excellent! If you plan to leave this talk early, the best reason to leave would be to watch this talk, which is available on YouTube on the Confreaks playlist for RailsConf 2014. In that talk, Sandy explores the Gilded Rose kata to teach some intriguing object-oriented design practices. I thought it would be interesting to use this code that I believe some might be familiar with to discuss good testing design and, specifically, effective RSpec practices.
00:05:38.960 Without further ado, I hope we accomplish a few objectives during this workshop. First, I want you all to leave with a clear understanding of the purpose and benefits of testing. Secondly, I aim to clarify what we mean by 'the object under test.' I want to convey how testing can be utilized to document code functionality effectively. Additionally, everyone should grasp the three-phase test pattern and understand that we should strive to write our tests in a manner that optimizes for readability.
00:06:13.280 Finally, I believe we should be cautious regarding the use of test doubles, which we will discuss further. Now, just a little disclaimer as a prologue to the workshop format: I used to be a teacher, and this format is inspired by that experience. While I've never taught in a classroom that looks quite like this, the idea is that I will demonstrate something up here, we’re going to do something together, and then you'll get a chance to try it on your own. This pattern will be followed throughout the workshop.
00:06:44.160 Perhaps this structure will work for you because the 'you do' portion will be just at the right level. However, it might also be too easy or too difficult, in which case, the documentation and the README closely follows the directions I will be going through in the workshop. If you feel the need to speed ahead, you are welcome to do so, and if we start moving ahead as a group, you have that documentation available to you.
00:07:01.199 Now, I would like to engage in some audience participation. I would appreciate someone brave enough to join me in discussing the first question: Why do we test? As a community, why should we all be present here right now?
00:07:31.680 To validate that our code does what the business requires. To catch bugs before QA. To prevent breaking my own code. To document the code. I think that covers a lot of the rationale behind testing. While some folks might offer different reasoning for their testing practices, I hope the things I've presented resonate with those of you who share similar thoughts. It's important to distinguish between two types of tests: unit tests and integration tests. A unit test targets a specific small portion of business logic, like a single function or class, whereas an integration test assesses the interaction between multiple components.
00:09:13.360 As a practical example, think of a unit test as testing a specific function, perhaps like counting participants in a workshop. An integration test, on the other hand, could represent the larger interaction of multiple systems. It's essential to mention that there are distinguished methodologies around units and integration tests. Another concept to introduce is the distinction between query methods and command methods. A query method returns something and changes nothing, while a command method doesn't return anything but does change some state.
00:10:09.520 I'll show you a method, and I want you to decide with the person next to you whether it's a query method or a command method. Talk it over for 30 seconds.
00:10:43.520 Okay, who wants to share their thoughts with the group? Yes, that's a good observation—it's a query method. Has anyone else thought about this?
00:12:10.960 Indeed, I think we've all been in situations where a method's name doesn't clearly indicate its behavior. Thus, distinguishing a query from a command can be tricky. When you have a query method, you want to test the return value. For instance, if I want to validate the workshop count, I should affirm that calling workshop count returns the value I expect.
00:13:00.400 Conversely, when dealing with a command method, you're testing the state change. For example, with an enroll operation, I won't check for a return value but rather test to ensure that the participant count has increased as expected.
00:13:45.440 Sandy Metz provided a helpful diagram for understanding this distinction, and we will discuss it further, including the topic of test doubles. Now, let's dive into the first workshop activity. The goal is to understand the object under test better. Often, when asked to implement new code or comprehend existing functionality, my first instinct is to look at the test file.
00:14:28.160 There's a prevalent belief that tests serve as a form of documentation for code. However, I find that it’s quite rare for me to look at an RSpec file and obtain significant clarity about expected outcomes. How do you all feel? Raise your hand if you usually don’t gain clarity from opening an RSpec file.
00:15:24.080 Many of us share a common struggle. At work, we sometimes host Turing graduates who are new to the tech world. I often find the most time-consuming part during their onboarding is trying to explain our spec files.
00:16:12.400 In these instances, I realize we can optimize the way we write tests to ensure clarity for ourselves and our new teammates. Now let me show you some code that's valid in RSpec. There's a keyword called 'subject.' Many of you are familiar with it, so for brevity, I won't go into detail.
00:16:40.640 When you don't explicitly define what 'subject' refers to, it can create confusion, especially for beginners or those transitioning from other languages or testing frameworks. How can we improve this?
00:17:27.680 We can make it explicit. The two examples I'm sharing are functionally identical, but in the improved one, I clarify what the subject is, allowing anyone reading it to quickly understand that we're focusing on a new Workshop object. In my RSpec practices, I recommend calling out the subject right up front.
00:18:09.840 Additionally, when I write RSpec, I name my subject and reference it in subsequent tests for clarity's sake. Now, I want you all to participate: open the repo I asked you to clone, and on line seven of the Gilded Rose spec, there is some poorly written RSpec meant to demonstrate how to improve clarity. Take a few minutes to see if you can reformat it.
00:19:07.640 If you have time, commit and push your changes, and then we can start discussing your code without putting anyone on the spot. Let's take about three minutes for this activity.
00:19:39.040 Another possible extension activity for those who are feeling confident already is to start applying the Gilded Rose kata. If you achieve bingo with your board, don't hesitate to wave your hand, and I will congratulate you. Just a reminder: if this feels too slow, feel free to dive into the bingo board or jump ahead.
00:20:18.080 Did anyone make progress applying the workshop example to the Gilded Rose example? Yes? Awesome! Essentially, it simplifies matters by removing the 'subject' designation and calling it out upfront, allowing us to focus on 'gilded rose' directly.
00:21:02.240 Is there a question or comment? Do you ever use 'describe class' instead of calling out the regular name? I actually appreciate using the 'describe class' keyword as it allows me to rename a class at any point without going through all my tests.
00:21:33.440 To recap, moving the subject out of individual tests and upfront where it can be easily referenced is beneficial. Instead of a generic subject keyword, a clearer name like 'gilded rose' makes the purpose of the tests more transparent.
00:22:26.640 In line eleven of your Gilded Rose spec, there's a test you might consider cleaning up with 'describe', 'it', and 'context' blocks. I encourage you to explore breaking it down for clarity and let's see what you come up with. You have five minutes for this activity.
00:23:16.240 Time’s up! Who wants to share how they incorporated 'describe', 'it', and 'context' into the test on line eleven? That’s a fantastic approach—segmenting based on specific conditions helps clarify the test's intent.
00:23:44.000 One effective method to improve readability is ensuring every test cell is associated with a clear, specific condition or state. I appreciate how you all are dissecting the tests according to different conditions, as this strengthens test documentation. Let's now pull up line eleven and review how we can make further improvements to our tests in alignment with the three-phase test pattern.
00:24:56.000 For clarity, the three-phase test pattern includes: 1) Arrange—setting up only what's necessary. 2) Act—performing the action under test. 3) Assert—checking the expected outcome. If a test starts 50 lines long, it complicates understanding what's actually being validated.
00:25:53.280 Let’s refine a long test into these phases. We can avoid unnecessary redundancy by focusing specifically on the actions and outcomes we want to validate within each test context. Streamlining tests can enhance readability and assist both current and future developers, making it clearer what is being validated.
00:26:51.440 A key aspect of testing is ensuring that we have meaningful assertions in place to avoid unnecessary redundancy. To reiterate, we're aiming to establish a clear three-phase pattern: setting things up, executing the action, and then asserting the expected state change.
00:27:24.400 Let's jump back to line eleven of our Gilded Rose spec file—try to refactor it to align with the three-phase test pattern we've discussed. Take a few minutes to think through how you could set up, act, and assert effectively.
00:28:20.640 What changes did you implement? Does anyone want to share their approach to restructuring the test? That's a great change to eliminate redundant assertions and emphasize clarity in outcomes.
00:28:45.760 It sounds like a productive discussion! Establishing contexts for various conditions provides transparency and sheds light on the underlying behavior of the code. Well done!
00:29:32.480 Moving on to the next segment, we're going to talk about shared examples. Please raise your hand if you've seen this feature used before.
00:30:10.480 Here’s how it works: you define shared behavior with an example and can reuse the shared example numerous times throughout your tests. This can create efficiency in writing tests. However, this approach can produce a lack of clarity for anyone reading the tests later. To ensure the readability of the tests, refactoring them to express the intent clearly and directly may be the better solution.
00:31:06.160 Instead of writing shared examples that may obscure the functionality being tested, you should document your expectations explicitly tailored to each test narrative. This will clarify not only the purpose but also the conditions being evaluated within each test.
00:31:43.840 Let's move on to discussing test doubles now. Raise your hand if you heard any enlightening concepts from earlier talks today related to test doubles or mocking. It's crucial to leverage test doubles judiciously in your testing strategy.
00:32:24.920 We use test doubles to create controlled environments where we can validate interactions, particularly when testing units in isolation. It's important to keep in mind not to overuse them, as they can lead to brittle tests and could obscure the intent of what you're aiming to validate.
00:33:29.200 Let's revisit the concept of discovering what your code should do through tests. Using test doubles effectively allows us to focus on designing tests that reflect the intended interactions and behaviors of the units without being overly prescriptive about implementation details.
00:34:12.320 In testing practices, we should avoid stubbing behaviors on the very object we are testing. For instance, if I need to validate whether a workshop sends notifications appropriately, I shouldn't stub that behavior on the workshop itself. Instead, consider structuring your tests around external dependencies that offer these services.
00:34:59.120 As we deliberate on our testing strategies, ensure you focus on using test doubles responsibly, balancing mock usages with actual behavior verification. Your test doubles should serve to illuminate the paths necessary to validate behaviors rather than overshadow them.
00:35:47.600 As we conclude, I sincerely thank all of you for participating in this workshop. We will wrap things up shortly, but if you are interested in further discussions related to testing practices or any other concepts we explored today, I will be around for the next 30 minutes. Don't hesitate to reach out, whether it's about the Gilded Rose kata or any other aspects of Ruby testing.
00:36:38.800 Once again, thank you all for being present. I genuinely appreciate the engaging conversations and collaboration. Let's continue to learn from each other and enhance our understanding of testing in Ruby!