00:00:10.160
Welcome everyone, we're going to get started with our next session.
00:00:11.040
You might recognize this speaker's voice. Stephanie is the co-host of the Bike Shed podcast.
00:00:14.879
She's a Thoughtbot developer, and she has a great talk prepared for you today.
00:00:17.240
I'm honored to be introducing Stephanie Minn.
00:00:32.559
Hey everyone! Thanks so much, Kevin. I am so excited to be here today.
00:00:34.559
I'm very thankful to be in the first slot, so I can hopefully enjoy the rest of my conference without the anxiety of giving this talk.
00:00:40.399
So, we're just going to go for it!
00:00:43.799
I recently learned about something called the stages of change model. It describes how people navigate making significant decisions or changes in behavior in their lives.
00:00:52.600
I suspect my talk landed in this room because you are somewhere around the contemplation stage, considering making a change but might not know how to go about it.
00:00:59.519
In the determination stage, you may be ready to take action, starting with some small, approachable steps.
00:01:02.320
If you are indeed somewhere around these stages, then that is great for me because that means I have the best possible chance today to help you move forward from where you're at.
00:01:09.240
Can I get a show of hands if you or someone you know struggles with testing and you're not really sure how to make it better?
00:01:17.000
Yeah, okay, most people in this room.
00:01:18.000
Great! You're in the right place. If you're already a pro at this, know this talk is still for you. I think we would be wise to better understand and emphasize the experiences of our fellow colleagues.
00:01:25.280
As a consultant, I embed in client teams dealing with complexity in their day-to-day work, including the challenges in their tests.
00:01:30.679
The following may sound familiar: you read or write a test and get lost along the way. You find yourself bogged down, brute-forcing a test to green.
00:01:36.240
It makes sense that that feels painful, especially if you work in a large and complicated codebase.
00:01:41.600
What if I told you that you already have a tool for determining the next step?
00:01:43.400
Our instincts are wise, and we would do well to become more skilled at listening to them. My goal for this talk is not to prescribe solutions, but to encourage how you might explore your own process for navigating a tricky test.
00:01:49.519
Today, we'll work through an example realistic to many modern Rails apps I've seen. We'll articulate the reactions provoked by the code, learn how to adapt, and make space to experiment with change.
00:01:59.080
Imagine we're building an app called Patreon. Yeah, it's like Patreon but for pets.
00:02:02.399
It's a platform for people to financially support their favorite pets in exchange for membership perks.
00:02:08.360
This is my dog Hickory. He has a page on Patreon to cultivate a community of people who want to shower him with love, affection, and treats.
00:02:13.159
You can join the club on a certain tier, each of which comes with different membership benefits.
00:02:16.559
A reasonable $5 a month gets you access to an exclusive album of cute pictures of him, or you can go wild for the $100 tier.
00:02:21.600
I will personally cook him a steak dinner every month and you can FaceTime with him because why not?
00:02:26.319
Since I clearly know what the users want, I've also built the functionality to let Hickory fans try out benefits before committing.
00:02:32.679
You can upgrade tiers on a trial basis. This happens via a class called TierWithTrialUpgrader that accepts a membership and a tier in its constructor.
00:02:36.959
It has one public instance method called upgrade which starts the trial and updates the membership tier.
00:02:41.840
There's also some conditional logic if the membership is on a subscription-based billing model, it updates that subscription with the trial that was started.
00:02:44.800
This is perfectly serviceable code if you catch my drift.
00:02:49.680
In apps I've seen, classes like this will orchestrate procedural work. Maybe this code logs an event or enqueues additional tasks in a background job.
00:02:52.560
Later it turns out that subscription-based members are upset because they didn't get proper notification of their trial, and they want to know when they'll be billed next.
00:02:59.720
So we find ourselves looking inside this condition. Let's say we implemented a class called SubscriptionNotifier.
00:03:03.560
It has a class method named notifyTrialStarted that takes in a subscription and a trial. We can presume that subscription is defined elsewhere in this file.
00:03:09.240
We have trial as a variable from earlier in this method. Since we introduced new behavior, we would like to write a test for this.
00:03:13.800
For this talk, we are using RSpec Rails and Factory Bot as part of our testing toolkit.
00:03:15.680
We navigate to the spec for this class and notice there is an existing test for the method and even the condition that we are targeting.
00:03:17.640
Except it looks like this.
00:03:19.120
I find it pretty overwhelming. I can't even make sense of this slide.
00:03:22.760
Just don't even look at it.
00:03:25.479
Let me tell you what I experience when I look at this code. First, I want to make a noise, like something's wrong.
00:03:30.239
I don't know what to focus on. I am surprised to see names of collaborators that I didn't see at initial glance in the application code.
00:03:35.120
I'm confused about arbitrary values in the test data. I don't know what's important and what's not.
00:03:38.560
I enumerate these feelings because once I do, I tend to observe them with less attachment.
00:03:41.600
That helps me step into curiosity about what's going on rather than wallow in my grievances.
00:03:44.160
If you've heard of code smells, they are indications that something may be off about the design of your code.
00:03:46.240
Regardless of whether it works or even passes a test, developing a nose for code smells takes practice.
00:03:48.960
We already possess the instrument to pay attention to our own state of being.
00:03:51.000
Remember how I mentioned surprise at seeing additional collaborators? It turns out these two methods were obscuring some implicit dependencies.
00:03:58.039
A subscription is a memoized instance of the class Stripe::Subscription, which takes in a membership. It turns out the update method is called on the subscription.
00:04:03.240
It accepts the names of tiers it's moving to and from, as well as the end date for the trial. Don't ask me why, this is just what we're dealing with.
00:04:07.440
So our goal was to test the behavior we added. What now? The existing test was so difficult to read, and I suspect difficult to write, that we had to dip into details of private methods to understand what was going on.
00:04:15.839
When you write a test, you are interacting with the code as a user. It's said that if testing is painful, you may want to reconsider the design of your code.
00:04:20.160
That begs the question, what are tests for? Sure, they verify behavior and catch bugs, but if they give us grief and we don't know why, then I think a mindset shift is in order.
00:04:24.239
I'm of the opinion that we should write tests for ourselves and for each other, not merely for a green check mark. A good test tells you how the system works, how to use the methods available to you, what to put in and expect out, and any side effects that happen along the way.
00:04:31.480
Think of it like leaving a torch in a dark tunnel. We pay it forward when we write tests with the intention that others, including our future selves, will rely on them.
00:04:34.479
So we find ourselves in the dark tunnel of the TierWithTrialUpgrader class, a little lost and uncertain, and that's okay. It's not anyone's fault.
00:04:36.959
What matters is what we choose to do next, how we find our footing.
00:04:39.480
The main feeling I want to resolve is that overwhelm. How can I ground myself in this test file?
00:04:45.919
I typically begin by reorganizing information. I might collocate behaviors by inlining references that are declared far away, such as in blocks.
00:04:48.320
I might flatten or nest depending on what I need to situate the context of the conditions that are being tested.
00:04:51.080
In this case, let's start with a clean slate and write the test from scratch. Even though there's already an existing test for this code path, I would prefer not to be biased by what came before.
00:04:59.240
This is our test now, and we can do whatever we like.
00:05:02.639
You might feel nervous to do this, and that's totally okay.
00:05:04.639
It could mean you need a little bit more information before getting started with writing a test from scratch.
00:05:07.440
Common sense tells us to get directions before we head off into the unknown.
00:05:10.960
I suggest asking yourself a few questions.
00:05:13.040
Do you know what you're testing? Do you know how to set up the object and method under test? Do you know how to get into the appropriate code path?
00:05:16.160
If not, you may have a bit more work ahead.
00:05:22.920
So once I've gotten my bearings, I follow the arrange-act-assert pattern for organizing my tests.
00:05:25.680
This helps me keep track of where we've been and what we still need to do.
00:05:28.239
Sometimes I begin with the assertion, because this is information we should hopefully already have at hand.
00:05:32.239
We wrote it in our test description. In this case, we want to verify the behavior of the SubscriptionNotifier.
00:05:35.720
Since it is a side effect, and we don't have really a state to assert on, let's mock our class and expect it to receive the method.
00:05:40.239
Next, we construct an instance of TierWithTrialUpgrader to call upgrade on it.
00:05:43.600
It needs a membership and a tier, so we provide those two arguments by building them with Factory Bot.
00:05:46.080
Since we want to enter the condition of a membership with a subscription, we set the has_paid_subscription attribute to true.
00:05:50.080
Lately, I've been finding it helpful to think about necessary versus unnecessary coupling.
00:05:54.760
Coupling is the degree to which things need to change together.
00:05:57.520
You may know it as something to avoid, because loosely coupled modular software is easier to change.
00:06:00.600
Some amount of coupling is expected in a test, however, because we need an entry point into our system.
00:06:02.880
Considering the minimum dependencies required primes me to notice when coupling starts to unravel.
00:06:05.240
So, I think we would benefit a little bit from understanding where we're at now. So far, we've coupled ourselves to the class and method under test.
00:06:11.400
Luckily, upgrade doesn't accept any additional arguments, so we don't have to set up anything else.
00:06:14.640
The arguments to instantiate the TierWithTrialUpgrader are the membership and tier, as well as the details required to configure them to get into the right code path.
00:06:17.880
Lastly, the class responsible for the side effect, and for now, that all makes sense and seems reasonable to me.
00:06:21.360
However, we want to ensure that notifyTrialStarted is sent with the correct subscription and trial.
00:06:25.240
So we use our specs with a matcher to constrain our expectation.
00:06:29.160
Now we have a problem: how do we specify these arguments when we don't have access to them in our test?
00:06:31.040
Remember, a subscription is defined as a private method. So we instantiate it in our test.
00:06:35.760
Unfortunately, in order to make an assertion on it, we have to swap it in by stubbing the Stripe::Subscriptions.new method to return.
00:06:39.560
There we go. Our test looks like this now.
00:06:41.840
It grew a couple of lines, and I feel a bit uneasy about the wizardry we had to do in order to stub the subscription.
00:06:44.080
I want to make a note of that feeling, but for now, we'll move on because we still need to set up a trial.
00:06:47.600
If you'll recall, a trial is returned by a class method called start on the Trial model. So, we build a trial with a factory.
00:06:53.760
Again, we need to stub a class method in order to verify that the trial was returned and that it indeed gets passed to our Notifier.
00:06:56.560
This is where I began to juggle a few too many things in my head, but we are so close.
00:06:59.680
We are ready to run our test, and it fails spectacularly.
00:07:02.240
Why? It turns out that updating a subscription actually makes a request to the Stripe API.
00:07:05.120
That test failure can manifest in a couple of different ways. You might see an error from Stripe itself.
00:07:09.680
If your app uses Webmock, you'll see a complaint about external network requests.
00:07:12.239
Or worse, what we just saw, you might get an obtuse NilClass exception.
00:07:16.320
Because the code is so brittle, you simply can't run this test in isolation despite setting up everything in the class.
00:07:18.720
Anyone? Sorry, this clicker is a bit finicky.
00:07:24.360
Okay, so fine, we don't want to bother with subscription actually calling its update method in the test.
00:07:27.600
And we plug that up here with another allow statement.
00:07:31.120
And now, if you can believe it, we finally arrived at a green test.
00:07:34.080
Though honestly, at this point, I'm pretty mad. That kind of sucked!
00:07:36.240
I've lost a lot of my steam. When I'm in this mood, pessimism blocks my critical thinking.
00:07:39.760
When I try to push through it, I do worse work.
00:07:43.760
I would like to interact with the code from a place of calm and not frustration.
00:07:47.760
So now is an excellent time to take a break, grab a snack, and pet my dog.
00:07:51.480
Look at this guy! Don't you want to join his Patreon? Yeah, $100 to moan!
00:07:56.480
Anyone? Anyway, we come back to our test in its full glory a beat later, and it's passing.
00:07:59.760
That's great! Let's take a moment to celebrate what we've accomplished thus far.
00:08:02.480
In testing, the notifyTrialStarted was invoked with the correct arguments. We made explicit the data required for the method under test.
00:08:06.279
We unearthed some coupling to Stripe via the subscription model, and we learned how trials are started.
00:08:10.799
Working effectively with legacy code, Michael Feathers introduces the concept of a characterization test.
00:08:13.520
When writing tests for code you don't understand, the goal is not to figure out what it should do, but what it actually does.
00:08:18.040
When you write tests that way, testing no longer is a moral authority; it's just a tool for facilitating the gathering of information.
00:08:22.519
I like this framing because it encourages discovery as the primary goal.
00:08:26.640
Even though we had some existing coverage, writing the test from scratch illuminated our code for what it was.
00:08:30.240
Whether or not we liked it, that brings me to my next point: move at the speed of trust in your tests.
00:08:36.680
If you don't have trustworthy tests, it's no surprise to me that fear of change lurks in every file.
00:08:40.160
It's not enough for tests to fail if you don't understand what broke.
00:08:43.760
That's why I take the time to care for and restore tests. It's the quickest and safest way to observe the behavior of your application.
00:08:48.160
Now that I'm no longer overwhelmed, I'm more in tune with other types of friction.
00:08:52.320
Let's say I start having to scroll up and down in this test file just to navigate it.
00:08:55.479
That's kind of annoying, right? What started as a few lines has now taken up a lot of real estate.
00:09:00.200
In other words, this class has grown additional responsibilities.
00:09:03.720
It may not be suited for the upgrade method, which now has to start a trial.
00:09:07.080
Determine if the membership has a subscription, instantiate it, update it, send the notification, and update the membership as well.
00:09:11.760
Writing out responsibilities of classes and methods can be in a code comment or on a Post-it note.
00:09:15.040
This is a technique commonly used in domain-driven development, and I find it useful when I need to make sense of existing code.
00:09:19.880
Without that awareness, I'm not loving this change.
00:09:22.080
The class already manages a lot of tasks, and my experience tells me that code like this only attracts more responsibilities.
00:09:25.959
You see it, and you're just like, 'Ah, what's one more line?'
00:09:29.160
I'm not totally keen on merging this as is because I don't think my work is complete.
00:09:31.959
We've built up all this knowledge, so let's put it to good use.
00:09:35.120
For me, that's a cue to seek out opportunities for improvement. With our trusty test in place, we can begin refactoring safely.
00:09:40.799
Here's what our test descriptions currently look like.
00:09:43.679
Their setups end up being quite similar, and now is a good time to ask: why does this duplication exist?
00:09:46.280
In a previous life, before I was a dev, I went to journalism school where I learned the value of brevity.
00:09:50.680
I often find myself noticing redundancy.
00:09:54.760
I've highlighted all instances of the words subscription and trial. Is there a missing connection here?
00:09:57.240
Are these operations maybe that have to happen together?
00:10:00.760
I also want to follow up on the awkwardness I felt from allowing these messages.
00:10:03.640
By doing this, we wave a magic wand over these methods so that they're not actually exercised.
00:10:06.239
When these allowances make up the bulk of my test, I get dubious.
00:10:10.640
How do I know that my code is integrated properly when I've just allowed all these shortcuts?
00:10:13.880
That feeling should nudge you towards a more focused unit test.
00:10:17.760
I chose an example with excessive stubbing because I think it's a common cause of pain.
00:10:21.680
There's a different style of testing that involves creating all the objects needed to exercise the code fully.
00:10:24.520
Rather than stubbing the update method, we could have set up some more collaborators.
00:10:27.320
Having to create many objects and corresponding records in the test database is a symptom of a similar problem.
00:10:30.840
While we're taking a different approach here, be wary of mystery guests that show up to the party in your tests.
00:10:34.360
So my doubt kind of turns my attention to the code. Isn't it surprising how these mere two lines ended up generating so many more in the test?
00:10:37.680
That leads me to inline the update_subscription method. Now I can see clearly what's involved.
00:10:41.520
My eye catches on how we are exposing the tier_name method on subscription.
00:10:43.679
Wouldn't this be available from inside the instance method?
00:10:48.399
If you get lost or distracted by seemingly irrelevant method calls, that's a signal that there may simply be too much information.
00:10:51.240
I had made a point earlier to keep an eye on extraneous coupling.
00:10:55.840
The failing test we saw earlier also reinforced that concern. We found ourselves deep in the guts of code dealing with the structure of Stripe's subscription API.
00:11:03.240
I know a rabbit hole when I see one. This code not only leaks details about subscription, but tier and trial as well.
00:11:09.120
With all this laid out in front of me, I wonder if there's a meaning hidden for me to discern.
00:11:15.520
Can I distill this information into a single idea? Many people associate redundancy with applying "DRY": Do Not Repeat Yourself.
00:11:23.960
I find that rule quite lacking in nuance. The point is not just to remove duplication; DRY requires sitting with ambiguity until you are able to express the truest and most precise abstraction with your code.
00:11:30.000
Anything less risks obfuscating meaning, like we saw earlier. To be clear, this is really challenging. It also takes time.
00:11:34.760
I enjoy pairing with someone to talk through it. Doing so also develops a common language for the code you're working on together, which is critical for sharing knowledge across the team.
00:11:37.360
After giving it some thought, I landed on encapsulating the two responsibilities in a new method.
00:11:40.520
We sprouted the idea of updating a subscription specifically in the context of a trial, as well as wrapping it with the intention of notifying.
00:11:45.280
Hence the name, notify_update_with_trial.
00:11:49.720
This is another strategy I like for working effectively with legacy code.
00:11:53.760
Wrapping methods lets us add new functionality to existing methods without modifying them.
00:11:57.440
We've also introduced a new seam. A seam is a place that enables behavior to change.
00:12:01.520
The seam we've made here separates the class responsible for orchestrating the mechanisms of upgrading from how it happens.
00:12:05.760
This seam dovetails nicely with another guideline: Tell, Don't Ask.
00:12:10.000
Previously, the code exposed information about the tier subscription and notifier.
00:12:14.400
Instead of querying for those details, we simply tell the subscription to do a notified update.
00:12:18.000
The best part is that we can basically move our existing test into the spec file for Stripe::Subscription with a few minor adjustments.
00:12:21.280
That leaves us with only one test needed for that condition in TierWithTrialUpgrade, and for now, this is where we stop.
00:12:26.080
I've been sitting with this example for a month now, and I still see areas for improvement. But perfect is the enemy of good.
00:12:30.240
That's a task for another day. This is what the method looks like now.
00:12:35.679
You might be thinking that was a whole lot of effort for it to look almost exactly the same.
00:12:37.360
Was it worth it? I believe so. The only way to deal with complexity is to manage it.
00:12:41.360
We created an affordance for the emerging concepts related to subscription and we prevented this class from growing yet another line, another responsibility.
00:12:45.600
We learned a lot and documented those learnings as collective wisdom in our tests.
00:12:48.560
Building up that knowledge is crucial. It's only then we can shape change with creativity and clarity.
00:12:52.720
Michael Feathers says the biggest obstacle to improvement in large code bases is the existing code.
00:12:55.840
But I'm not talking about how hard it is to work with difficult code. I'm talking about what that code leads you to believe.
00:13:01.200
Code constantly provokes reactions in us, for better or for worse. It doesn't even need to be old; it can be code you wrote ten minutes ago.
00:13:05.000
How we feel when we write tests is one of the quickest feedback loops we have about the quality of our system.
00:13:07.840
If we ignore or numb ourselves to that pain, we risk losing faith in our work.
00:13:11.120
Or worse, we may be told to do the dreaded rewrite.
00:13:12.919
We need to find better ways of coping because that test pain matters.
00:13:17.000
Can you figure out what you need to do right now to attend to it?
00:13:21.240
It doesn't need to be a sweeping refactor; small actions add up.
00:13:25.239
I promise, even just reorganizing some stuff in a file, verifying the name of a method or variable, that helps you.
00:13:30.000
It helps others, and that's important.
00:13:31.920
So we can continue doing the good work of connecting pets and pet lovers on our obviously viable and profitable product, Patreon.
00:13:37.760
To call back to the stages of change model from the beginning, I would be thrilled if I were able to help you today.
00:13:42.480
Move even just a little bit forward towards determination and action in harnessing the power of your tests.
00:13:48.240
If you're ready to dive deeper, I learned, referenced, and found inspiration from many resources for this talk.
00:13:51.520
I'm excited to see a TDD for absolute beginners workshop on the schedule for tomorrow.
00:13:56.480
If you have yet to try test-driven development, I highly recommend it. It's a great skill to have in your testing toolkit.
00:14:02.480
All of the talks in this room today go really nicely with themes around software design.
00:14:06.560
I will be staying in this room for sure.
00:14:10.479
We talked some today about design patterns and heuristics.
00:14:14.640
In fact, object-oriented programming picked up the idea of design patterns from the art and science of architecture.
00:14:18.360
Specifically, the work of an architect named Christopher Alexander.
00:14:22.760
He wrote a book called The Timeless Way of Building that theorizes why design patterns work so well for us.
00:14:27.960
He says that the point is to invoke a quality without a name.
00:14:30.360
He illustrates this idea by distinguishing between the design of a courtyard that feels alive versus one that feels dead.
00:14:35.200
A living courtyard is one that supports life. Its design enables our natural walking patterns, connecting to open spaces.
00:14:38.883
In contrast, a dead courtyard is one whose design fails to provide for the needs of the people in it.
00:14:43.520
It might be completely enclosed or provide no shade.
00:14:46.880
I'm sure we've all been in a courtyard and thought, 'Oh, I don't know, I just want some fresh air, but I don't want to sit here.'
00:14:50.000
It's uninviting and, as a result, becomes abandoned.
00:14:53.240
Our human intuition can tell that difference when we inhabit the spaces, and the same is true of the code we work in.
00:14:57.000
The tech industry loves being data-driven, but I personally find inspiration in remembering the purpose of best practices beyond what's clean or superior.
00:15:01.280
It's something that no algorithm can quite capture.
00:15:04.760
Danella Meadows, in Thinking in Systems, says that pretending something doesn't exist, even if it's hard to quantify, leads to faulty models.
00:15:09.680
Human beings have been endowed not only with the ability to count but also the ability to assess quality.
00:15:13.640
So let's not lose sight of that quality without a name.
00:15:17.280
How we feel about our software.
00:15:21.360
Since you're already writing a test, ask yourself: what do you need to find some peace and ease in that work?
00:15:24.720
And then make it so.
00:15:28.160
I'm Stephanie Minn. I work at a consultancy called Thoughtbot. You can hire us to help your team write joyful, attainable tests.
00:15:37.760
I host a weekly podcast about software development called The Bike Shed. My co-host, Joel Kinville, who's sitting in the front row, is also giving a talk along with several other Thoughtbotters on Thursday.
00:15:41.960
So do check those out! I would love to know what else you need to attend to your test pain and change code with confidence.
00:15:45.360
If you have any feedback or thoughts, I will be around. Please feel free to come up and talk to me.
00:15:51.040
I would really love that. And then maybe one day I'll launch Patreon for real! Some early users in this room!
00:15:56.960
Yeah, and also maybe Hickory will learn to write my tests for me.
00:15:59.720
And make this talk totally moot. Who knows?
00:16:02.560
And that's it. Thanks so much!