00:00:00.299
(foreign)
00:00:11.300
Welcome to 'TDD on the Shoulders of Giants.'
00:00:14.219
I'm Jared.
00:00:17.580
When I was first getting started with Ruby, I read a book called 'Growing Object-Oriented Software, Guided by Tests.' It’s by Nat Pryce and Steve Freeman. This book was really influential to me and broadly in the industry.
00:00:22.080
It laid out a style of testing that came to be known as London style, or Maucus TDD. The alternative to that style has several names, known as classical Chicago style. However, I’m going to refer to it as Detroit style in this talk.
00:00:30.359
Don't worry if you don't recognize those terms. The fact that they have so many names is probably a testament to how infrequently they are discussed.
00:00:37.740
The point is that the book laid the foundation for concepts like Behavior Driven Development, which eventually influenced tools like RSpec.
00:00:41.219
Recently at my company, Super Good, we held several book clubs where we all read this book in different groups. We explored how we could apply the insights from the book to our day-to-day Ruby practice.
00:00:45.719
I had three major takeaways from the book. First, the name of the book is quite long, so I’ll use the abbreviation 'GOOS' that we adopted in the company. Secondly, I realized that I had missed or misunderstood a lot of the wisdom in the book when I first read it.
00:00:55.380
Revisiting it after ten years of working with Ruby on Rails provided me with a different perspective. Finally, I found that some of the techniques were somewhat challenging to apply in Ruby since all the code examples in the book are written in Java.
00:01:03.420
This disparity in the languages is where the idea for this talk originated.
00:01:06.720
I want to provide you with actionable tips, strategies, and techniques from the book, translated into Ruby terms that you can use to derive greater value from your testing practice.
00:01:11.460
However, I will warn you that I'm not going to teach you London-style testing here. The book walks through a lengthy example over many chapters, and if you want to dive deeper into that, you'll need to read the book yourself.
00:01:19.200
We will also focus specifically on RSpec. While you can apply these techniques in any testing framework, RSpec has certainly been influenced by these techniques.
00:01:24.240
Occasionally, you might hear folks mention terms like 'strict TDD' or 'real TDD.' These terms can serve as red flags. TDD is not the end-all-be-all of software development techniques; it won't resolve all your design issues.
00:01:31.500
It doesn’t replace your experience or design intuition. TDD is meant to complement other design techniques in your toolkit.
00:01:36.660
The authors of 'GOOS' are very clear about this; they introduce other techniques to address problems in their working examples where TDD doesn’t offer solutions.
00:01:46.680
TDD is often simplified to Red, Green, Refactor: you write a test, run it, see it fail, write the minimum code required to make the test pass, and then refactor it until it integrates into the design of the system.
00:01:52.739
So, what’s with these different styles of TDD? I struggled to find a concise explanation of the differences between them, as there’s a plethora of conflicting information available.
00:02:01.500
I eventually stumbled upon a post on Mastodon that mentioned the distinctions: Detroit style tests are primarily refactoring tools intended to aid you in refactoring your code without typically relying on mocks.
00:02:14.760
Conversely, London style tests act as design tools. They emphasize individual objects and their interactions with collaborators, writing tests around those interactions while using mocks.
00:02:23.640
These mocks, known as test doubles, facilitate assertions about which methods are invoked and what arguments they receive in those interactions.
00:02:37.560
The post described various other styles, including one where everyone tests solely because they are instructed to do so, leading to less than ideal outcomes.
00:02:41.919
The London style exemplifies an outside-in approach to TDD, starting with an acceptance test before gradually building functionality through tests until the acceptance test passes.
00:02:52.320
During this process, you might encounter terms like acceptance tests and unit tests. Although we're not going to debate what's classified as a unit test here, understanding the broad strokes is important.
00:03:01.500
Acceptance tests check that our application behaves as intended, but they are not designed to dictate the structure, merely ensuring everything functions as expected.
00:03:14.340
These tests, being slow, won't check every branch of your features. However, they serve as a safety net. More critical applications may require tighter safety nets, while less critical ones can be softer.
00:03:29.280
Unit tests are the essence of your TDD practice in London style. They provide various benefits and serve first and foremost as design tools, allowing for stepwise component building driven by tests.
00:03:42.599
By listening to your tests, you can identify design problems. This central idea of the book could be the basis for an entire series of talks, but the core concept is that difficult tests—whether hard to write, change, or understand—signal design issues.
00:03:57.720
So if you're skeptical, reading the book may change your perspective. In organizations that prioritize testing, hard-to-write or change tests lead to complications.
00:04:06.300
The goal is to test your objects in as isolated a manner as possible. Using mocks to achieve this isolation results in tests that focus on message-passing.
00:04:19.800
These tests can scale efficiently, allowing for numerous fast-running tests without guaranteeing that your software operates correctly; this is where you can see unit tests pass while integration tests fail.
00:04:29.700
Memes about drawers that won't open, drains that don’t drain, or windows with handles blocking others highlight common frustrations in such scenarios.
00:04:37.740
Next, let’s discuss integration tests. These are positioned between unit tests and acceptance tests. They evaluate multiple units working together and their ability to deliver desired outcomes.
00:04:48.300
While TDD doesn't adhere to rigid rules, it does emphasize one important guideline: never write new functionality without a failing test.
00:05:02.760
Whether modifying an existing test or adding a new one, that’s the starting point. Ideally, you’ll make incremental changes, one test at a time.
00:05:09.840
Although this is a guiding principle, you're encouraged to utilize other design techniques as necessary. Not adhering to this rule doesn’t mean you’ve failed at TDD.
00:05:15.420
This technique is advantageous for various types of issues but may be unhelpful for others. TDD is fundamentally a design tool and won’t assist with code optimization.
00:05:29.100
In essence, London-style TDD begins from the outside, crafting acceptance tests and utilizing them to progress on high-level features. Unit tests not only build functionality but also shape design.
00:05:38.880
We focus on assessing how your objects interact with their collaborators, utilizing mocks to maintain test isolation while gradually assembling functional components.
00:05:44.520
Although there’s much more nuance in this quick overview of the technique, to learn further, I encourage you to read the book.
00:05:51.360
Early in the book, we learn that when using mock objects in our tests, we should mock their interface types and not the classes themselves.
00:06:02.700
This proves to be slightly challenging for us Ruby developers, as Ruby doesn’t have interface types. Java interfaces describe methods, return types, and arguments that a class must implement.
00:06:13.560
Mocking interface types compels you to write code compatible with those types rather than with a specific instance you might be passing in.
00:06:21.540
You can easily substitute different objects adhering to that interface later without altering your code.
00:06:33.120
This practice helps decouple you from the current structure of your application.
00:06:36.540
When I refer to the structure of your application, I mean how the objects are presently arranged, forming a web of interconnections that comprise your application.
00:06:54.600
In Ruby, we need to reflect on why this advice was given. The authors encourage us to design our programs such that we can swap objects for different ones without needing to modify our code.
00:07:06.120
Ruby's duck typing allows us to replace objects with new ones of a compatible structure, even though it lacks the security offered by interface types.
00:07:11.700
So, while we may not need to heed this advice strictly, there are still valuable lessons to learn.
00:07:20.340
Let's take an example from the e-commerce domain. When a customer orders something online, the items are generally delivered in one or more shipments.
00:07:35.640
When we dispatch a shipment from the warehouse, we usually send a notification email to the customer.
00:07:44.220
Here's a simplified code snippet that could hypothetically handle this. Our Shipment class has a 'ship' method that takes an 'EmailNotifier' instance to which it sends some shipment information.
00:07:54.300
If this were Java, we would have to define a type for the EmailNotifier, creating an interface that leads to the question of what we should name it.
00:08:05.520
When analyzing the interface, we find that it has a single method 'send_notification' that takes shipment info, leading us to realize that this is essentially a shipment notifier.
00:08:16.320
In Ruby, we can just switch the implementation without these constraints. But without interfaces, we often miss out on recognizing the need for them.
00:08:24.900
As Rubyists, we have to work harder to acknowledge these interfaces and make them explicit. When defining arguments, variables, or mocks, consider the interface of the object, not just its current role.
00:08:37.560
By doing this, you can make subtle changes uncover hidden interfaces, creating more opportunities for utilizing polymorphism and achieving testable code.
00:08:45.840
This links to concepts we've discussed in previous talks.
00:08:50.520
Moreover, if your organization transitions to sending notifications via SMS, you won’t have to rename the variable—an added benefit!
00:09:02.520
Previously, I mentioned application structure and the web of objects. The book strongly advocates building software from these tiny, focused, and decoupled objects.
00:09:16.140
You then compose and arrange these objects in various manners to construct the functionality of your application.
00:09:27.420
The book encourages us to engage in this composition at a high level, picking a single entry point within the system where these objects can be integrated.
00:09:39.300
Typically, if you’re using a framework, you want to identify a point where the framework transfers execution to your application code. In Rails, for instance, you don’t have control over how controllers are instantiated.
00:10:01.080
Requests flow through layers of framework magic before being handled by a controller action. Background jobs can serve as excellent locations for wiring all these objects together.
00:10:11.640
However, unit tests in those spaces become challenging since you can’t input mock objects easily. Yes, you may use RSpec stubbing, but that doesn’t yield extremely valuable tests.
00:10:23.520
Instead, glucose requires coverage through acceptance or integration tests that ensure the correct setup of components without testing every possible outcome.
00:10:38.520
I found this point fascinating because many teams struggle with this one. They tend to create excessive controller request specs that become sluggish or difficult to maintain.
00:10:46.920
Thus, this insight presents some leeway; we can afford to write fewer controller tests. However, in scenarios with Rails controllers, we still need to deal with HTTP at that level.
00:10:58.440
Sometimes, it’s impractical to extract logic, so while it's possible, it's often not advisable.
00:11:09.300
I previously overlooked this detail during my initial reading of the book, which explicitly recommends against fighting the framework you are using.
00:11:17.880
If a framework expects a specific approach, diverging from that will only generate unnecessary friction. Instead, embrace the advantages your chosen framework offers.
00:11:30.300
There’s a convention for structuring tests known as Arrange, Act, Assert—sometimes referred to as Given, When, Then.
00:11:43.800
This methodology segments your test code by function: you group the setup, execute the code under test, and then make assertions regarding the outcome.
00:11:56.520
'GOOS' isn’t the sole book promoting this method, but it’s a nice convention that facilitates navigation and comprehension within tests.
00:12:05.460
It also aids in detecting potential test smells. For instance, excessive setup code might indicate an object that’s hard to utilize, while too much verification hints at excess responsibilities.
00:12:12.300
Some testing frameworks make establishing such tests a bit tricky, particularly when verifying mocks, but RSpec offers clever solutions.
00:12:30.480
Consider this hypothetical test for the code we've discussed earlier. It begins by setting up a double for the shipment notifier at the top, establishing an expectation about the messages sent.
00:12:44.520
However, it proves potentially troublesome if the expectation is defined in a before block, as this would cause all tests to fail if that expectation fails.
00:12:56.520
Instead, configure messages directly on the shipment notifier, making this a command rather than assessing the return value.
00:13:02.640
You should transfer the expectation to the appropriate test, maintaining the integrity of expectations while allowing other tests to succeed, even in the event of a failure.
00:13:11.460
This organization follows our convention and alleviates one of the annoying points that new RSpec users often encounter. It’s confusing to assert something has been invoked before it's actually executed.
00:13:29.460
Now let’s shift gears and discuss values—the kind not imposed by your company on retreats!
00:13:31.500
I'm referring to literal values like 0, 312, or false. Values are essential in testing outcomes of simple computations. Admittedly, applications involve more intricate operations.
00:13:43.500
Value objects, on the other hand, represent values without significant identity. They often embody more complex data, such as money.
00:13:57.300
Money isn’t merely a numeric value; it entails a currency and can have constraints on usage. In Ruby, there's a library called 'Ruby Money' that facilitates working with monetary amounts.
00:14:09.480
If you have two ten-dollar amounts, they can be interchanged. In the realm of crypto, they are fungible. The essence is that these values lack a concrete identity.
00:14:21.240
Ruby is equipped with various useful value types, such as dates and ranges. We've seen some in recent talks, but you can also create your own to accommodate your application’s requirements.
00:14:32.340
For example, Joel demonstrated crafting month objects when standard date objects weren’t sufficient.
00:14:48.300
In e-commerce, I frequently work with variables like money, prices, lead times, and inventory counts—all of which can be characterized as values.
00:14:59.280
By structuring these collections of data into objects, we can develop more expressive tests, enhancing both the clarity of tests and the application's code itself.
00:15:13.080
When executed properly, this approach facilitates higher-level abstraction while minimizing intricacies, especially surrounding data handling.
00:15:22.920
So how do we accomplish this in Ruby? We can create structs. For instance, we can define a struct with 'red,' 'green,' and 'blue' members.
00:15:31.920
Structs inherently support equality, thus functioning as effective value objects by default.
00:15:38.520
You can enhance them with methods, such as a method to darken the color. Importantly, this change returns a new object without modifying the existing one—consistently upholding immutability principles.
00:15:49.440
In the previous talk, we discussed that mutability could lead to chaos. Imagine being able to change the number two into three; it would be complete disorder!
00:16:04.680
Structs aren’t without drawbacks; they allow members to be altered, which can negate the immutability we desire.
00:16:11.520
Fortunately, Ruby 3.2 introduces a new feature: data classes. Defining a data class is similar to creating a struct but allows flexibility—, you can utilize either positional or keyword arguments.
00:16:20.460
Data classes retain the same quality features as structs and allow for defining custom methods, pattern matching, and other features, but you cannot reassign attributes.
00:16:31.440
Thus, value objects enable you to build more expressive code at higher abstraction levels, an area where Ruby's capabilities are flourishing.
00:16:39.840
Pay attention to discover moments to employ them. When you note clusters of data being passed around, it’s time to ask if it warrants a value object.
00:16:43.860
If it does, wrap it in a struct or custom class, or, if lucky enough to be using Ruby 3.2, a data object, and refactor your application to leverage these.
00:16:53.640
In your tests, there's no need to mock these values. Mocks are appropriate for isolating interactions between different objects, while values merely encapsulate data.
00:17:02.760
Focus on producing new values stemming from that data, just like you wouldn’t mock the integer three.
00:17:09.300
If you find yourself frequently using values in tests or if setup becomes cumbersome, consider using Factory Bot for value-creating factories.
00:17:18.240
Factory Bot isn't limited to creating database records; it can generate any object type. If it enhances your tests’ expressiveness and readability, utilize it!
00:17:26.520
Many languages lack the flexible testing tools that Ruby provides. One notable reference is Michael Feathers' 'Working Effectively with Legacy Code.'
00:17:33.600
This book addresses how to bring untested code—termed legacy code—into a testing environment, which can be complex, depending on your tools and language.
00:17:45.720
In contrast, Ruby’s nature, alongside tools like RSpec, makes testing more manageable for Rubyists.
00:17:52.620
You may have encountered tests structured this way, heavily reliant on stubbing. It becomes cumbersome to allow any instance of those dependencies.
00:18:03.960
Tests can rapidly become brittle, locking you into the current architecture of your system. Any change to these dependencies necessitates a change in the test.
00:18:14.760
This scenario runs contrary to our objectives. If you often need to stub methods on globals or entire class instances, it indicates that your components require dependency refactoring.
00:18:20.200
Before you proceed to stubbing methods or utilizing the flexibility of Ruby’s testing tools to navigate around these dependencies, assess whether these dependencies need to be loosened.
00:18:30.640
Our tools may offer us ways to avoid design problems, but they're not the ultimate solution. Use everything available if a class or module goes untested and requires coverage.
00:18:40.640
TDD is a design tool. If you encounter existing code with testing complications, embrace those complications while seeking opportunities to enhance the code.
00:18:50.600
I adhere to an extreme programming philosophy: when doing something, I aim to extract maximum value from it. This idea extends to testing as well.
00:19:01.900
The value of tests comes to light when they fail. Green tests indicate that no problems have been detected, but they don't imply everything is functional.
00:19:12.920
Thus, the red and green steps aim to illuminate failures that tests are intended to unveil. Red tests offer detailed insights—providing explicit information on how they failed.
00:19:23.240
With RSpec's versatility, you can enhance failure information with custom matchers which create expressive tests; a technique the book advocates strongly.
00:19:38.300
You can also append helpful messages to any matcher failures. I recently encountered a test that failed, but the message was very clear about the cause.
00:19:50.560
This practice reinforces the notion of clarity in failure messages, allowing for easier tracking of issues and resolution.
00:20:01.020
While most assertions do not need detailed messages—thanks to RSpec’s clear output—certain assertions might not inherently communicate your intentions.
00:20:12.360
This is where you can integrate custom failure messages. Additionally, it’s vital to select RSpec matchers that appropriately represent what you're testing.
00:20:27.600
Avoid overly strict assertions, as they can lead to false test failures. RSpec also offers matchers for comparing collections without implying order.
00:20:40.560
Keep in mind that you shouldn't strive for exhaustive tests; only assert those elements that matter to your objects and their collaborators.
00:20:52.680
Further, maintaining the brevity of your tests is advisable. Small tests tend to be clearer and easier to diagnose.
00:21:04.680
Invest time into creating context and descriptions in your examples. Avoid vague assertions like 'it returns the correct value' without stating what that value is.
00:21:15.720
Lastly, when a test fails, it’s crucial for the next developer to comprehend it easily. Complicated tests filled with unnecessary features can hinder understanding.
00:21:24.900
Assist that developer by writing tests you would feel comfortable reading.
00:21:37.860
Ruby offers robust testing tools tailored to an object model that harmoniously complements TDD. I’ve only scratched the surface of the valuable principles to glean from previous object-oriented programmers.
00:21:54.720
However, my hope is that some of these concepts will boost your testing practices. Remember to focus on object interfaces, avoid overly ambitious testing, work harmoniously with frameworks, and create clear, concise tests.
00:22:05.520
Leverage value objects effectively, maximizing RSpec’s features, and design tests that yield precise, valuable feedback.
00:22:18.840
In closing, I'm Jared. You can find me online.
00:22:21.960
Please connect with me on platforms like Twitter, Mastodon, or my company’s website.
00:22:35.160
Feel free to approach me after the talk for any questions; I’m always eager to chat. Thank you!