Check out a free preview of the full Software Developer Success: Soft Skills & Testing course
The "Refactoring Without Breaking Tests" Lesson is part of the full, Software Developer Success: Soft Skills & Testing course featured in this preview video. Here's what you'd learn in this lesson:
Francesca refactors the code in order to prepare it for a new feature. She uses a technique called "seam" to safely inject the new code without breaking existing functionality. She also demonstrates how to use the debugger to identify and fix a problem in the code.
Transcript from the "Refactoring Without Breaking Tests" Lesson
[00:00:00]
>> Francesca Sadikin: So we have all of our tests. We're not going to touch the new item first yet, because we need to refactor the code. So I'm gonna close this window. I'm going to move this one over to the right,
>> Francesca Sadikin: Just so that I'm always going to be referencing this Test Suite as we go.
[00:00:22]
So now, let's actually look at the source code. So we have a item class, there is a shop class in which there is a constructor. These are the items we have been giving it, and then there is a single method called updateQuality, right? So inside of this method, I see that there is a single for loop that's iterating through all the items, making some sort of changes and then it returns all the items, and that's what we've been seeing, correct?
[00:00:57]
Inside of this, based off of the name of the item it is making changes to the quality and the sellIn value. So looking at this mess, I don't think I want to be implementing my new code into the structure of what this is. It's just going to be too complicated, and so I'm actually going to refactor this code now to prepare it for the new feature.
[00:01:30]
Some notes here, everybody's resulting refactoring is going to look different cuz everyone has a different style, let's see. We're always going to be referencing the Test Suite every time we make a change to see what broke, and what didn't. And the thing that I'm going to use here is to use a seam.
[00:01:56]
So this is a very fancy way of just trying to say, we want to safely inject our new code without breaking a bunch of other stuff at the same time. And so what that scene would look like in this case is, let's see. So I see all of these conditions, I'm going to write a new condition here and I'm going to say here, this.items(i).name.
[00:02:24]
I'm going to handle a specialty item first just to make it easier for myself. I'm gonna call it Aged Brie. So all of my new code is going to be in this block and I'm going to create an else and put all of the existing code inside of it.
[00:02:47]
So what this does is that I now expect if I click Save Aged Brie, so actually I'm just gonna click Save for you. So what I'm seeing now is that Aged Brie is failing, but all of the other behaviors are still working. So this is what I mean by creating a scene where we're going to just figure out a place to safely put our code, but let everything fall back to the existing system.
[00:03:18]
So we're not gonna touch that, right? So in this case, we have Aged Brie, that code that we need to make pass. So when I refactor, I try to do it in iterations. I try to have a specific thing that I'm focusing on first. I'm not going to think about the most perfect code and then try to create that from the very beginning, so we'll do this in iterations.
[00:03:48]
The very first one that I'm thinking about is when I look at all of this code, I can see that relevant pieces of code for particular items are everywhere, right? You can see something related to Backstage passes in three places. And so the goal for this one is that I'm going to just consolidate all of the code relevant for every item.
[00:04:15]
And I really don't even need to [LAUGH] reference the existing code, I'm going to just write my new code that makes these tests pass. So the very first test that I'm looking at here is that it needs to decrement the sellIn value, so let's do that. I'm gonna write this.items(i).sellIn, I'm going to decrement it by 1 inside.
[00:04:43]
So we still have a failing test, but the assertion line has moved, right? The sellIn is now passing, but we need to make the quality also pass, so it's supposed to increment. So this.items(i).quality, I'm going to just decrement it by 1, okay? So that first Aged Brie test is passing, but the one about never being more than 50 is failing.
[00:05:14]
So I'm just going to write that this.items(i).
>> Francesca Sadikin: If the quality is less than 50, I increment, otherwise, I do nothing. And in all cases, I decrement the sellIn. So we have Aged Brie now. So let's move on. We now have to handle Sulfuras, so we're gonna do the same thing.
[00:05:47]
We are going to write, actually, you know what? Before I move on, let me actually just make a refactoring opportunity right here, cuz I can tell I'm going to be typing this part of the code over, and over, and over again and I don't wanna do that. So let's actually save that out into a new variable called item.
[00:06:08]
So here this.item(i), and what I'm doing is I copied item. I'm highlighting this.items(i) and now I'm using command D. I'm on a Mac, so Command D and that's Ctrl+D on Windows. And you can see that it is now selecting the next match, and I can control how many matches I want to do and I'm pasting.
[00:06:32]
So that's just a really handy refactoring tip in VS Code. Okay, so everything's still passing even with this refactoring. So now, I'm gonna handle Sulfuras. So let's add Sulfuras right here.
>> Francesca Sadikin: So I'm gonna to create that block, and I'm going to click Save. Sulfuras passes, that's telling us that we don't need to write any more code.
[00:07:04]
And this is one of the really cool things about test-driven development is that you sometimes write simpler code, because your test will tell you when you need to write that code. And then this case, all we needed was to do this and we're done. So now, we have the Backstage passes.
[00:07:26]
So here, let's write else if (item.name === Backstage passes. We've got the syntax, gonna click Save. I'm just making sure that, yeah, my other stuff is passing. Now, a bunch in Backstage passes is failing. So let's write our code. I'm going to just start from the top here, okay?
[00:07:53]
Here we go. Actually, the first line that's failing is that sellIn is supposed to decrement as expected. So item.sellIn decrements by 1, let's make that pass. Coming back up, now it's quality. Quality is supposed to increase, so I'm just going to just write item.quality,
>> Francesca Sadikin: Okay, increases by 1.
[00:08:28]
>> Francesca Sadikin: Okay, scrolling up. So that first one's passing, we have another one. It should never increase more than 50. I can see because we wrote these tests that this is going to keep happening over and over again, that we need to do this check. And so what I'm going to do is that up here is where I'm going to be manipulating quality, and I'm going to just check that if item.quality is more than 50, I'm just gonna set it back to 50 here.
[00:09:07]
So when I look at the Test Suites, all the ones related to never more than 50 is now passing.
>> Francesca Sadikin: So we have another one. We should increase quality by 2 when the sellIn is between 10 days and 6. So I'm gonna just wrap this. So again, with test-driven development and refactoring here, we always just write the simplest amount of code to make it pass.
[00:09:33]
It might look super simple, but we have a chance to adjust it and make it nicer in further refactoring opportunities. So in this case, just simple if conditions is fine. So if item.sellIn is more than 10, right, so 11 days or more. Actually, let's make that explicit, so 11 days or more, I increase by 1.
[00:10:03]
Else if item.sellIn is less than or equal to 10, item.quality increases by 2. Okay, so that one's covered, and then we have the one about incrementing by 3. So I'll add another case, else if item.sellIn is less than or equal to 5, item.quality is decrementing by 3, except it is still failing.
[00:10:41]
[LAUGH] So it is still decrementing by 2, I mean, sorry, not decrementing. Incrementing, incrementing by 2, but we are expecting it to increment by 3. So I'm gonna use this opportunity to transition into a small overview on the debugger. So let's use the debugger to briefly figure out why this problem is happening.
[00:11:11]
So I'm gonna go back into this particular test that's failing here, and I'm gonna add Breakpoints. You can see I clicked it over here, and this is telling the debugger where it should stop when it runs the debugger. From here, let's actually run it. So I'm clicking the Debugger button.
[00:11:35]
So I'm going to introduce the debugger to you, because when I was learning how to code, I never learned how to use the debugger correctly. And it was a huge regret of mine, cuz I just used console.logs everywhere and I finally realized how powerful it is. [LAUGH] So just to make sure that everyone does see what this is all about even briefly, I'm going to introduce here right now.
[00:12:00]
So the debugger window, again, way more powerful to understand what's happening in your system, much more powerful than console.logs. What you're seeing here right now, it's pausing at the top of the file. In this variables window, you're going to see everything that's in scope all the way up to Global.
[00:12:20]
So, for example, if I click play, it's gonna move to the next Breakpoint. Actually, in my test, you can see that the Variables window has now updated. Now, there's not just the Global scope, but there is all the other closures, including the one that's in the test scope itself.
[00:12:41]
Currently, everything is undefined and that's just because we haven't actually moved execution yet so that it hasn't been assigned anything. Other little things you'll notice here is this Watch window, so you can add any variable here. It's just a way to pin something to always watch it as things move around.
[00:13:04]
Again, gildedRose has undefined, cuz we haven't set anything. There is also a Call Stack window. So this shows you all the functions that are being called to get to this current phase. So this is where we are right now, you can actually click up the Call Stack and it will show you every single line that was called.
[00:13:32]
In this case, this is all just internal, and we don't really need it for this exercise. I'm gonna close this Call Stack. And then we have our Breakpoints here, just showing us all the current Breakpoints and we can easily turn them on and off. The other little thing that I want you to point out here is this little window, you can't really see it cuz it's blue on blue, but these are the controls for the debugger.
[00:13:57]
So you saw me click the Play button, so that's just going to continue execution until it sees another Breakpoint. We have the step over, so this is just going to move line by line. And then if you wanted to go into the internals of a particular function on that line, we'll do the Step-in button, and then there's also a step-out to come out of it.
[00:14:24]
We can also click restart, so that just restarts the entire debugger for these sets of Breakpoints. And when we're done, you click Stop. So for now, I'm just gonna show you this step over function. So you can see I stopped here at gildedRose, nothing is being shown here.
[00:14:44]
But once I click step over, we can see gildedRose immediately got updated with what the gildedRose shop is. And I can also hover over it and will show you here as well. So just so much more information than a console.log. So in this case, I'm on gildedRose, updateQuality, items hasn't actually finished executing, but I wanna go into this function, right?
[00:15:11]
So I'm gonna step into it. It brought me straight into the method, and it's in this loop. So I'm just now going to click Step over until I get to the point where I want. So it's moving through, and you can see all the variables here being updated as we go.
[00:15:32]
You see here, now it knows we're working on this particular item. And then it's like, no, not that condition. Yep, it's probably gonna be in this condition and here we go. So we're inside this block now, so we can see that sellIn was 5, right? So it's not gonna meet this one, move over.
[00:15:56]
I want it to be here, but when I go through it this way, I can see that like, yeah, my item selling is 5, it is less than 10. I'm in this conditional. This is why I'm not getting to the increment by 3 block here. It's because I wrote my conditional wrong here.
[00:16:17]
So that was a little intro to the debugger. I have figured out that this is my line that is causing me problems. So I'm gonna exit out of the debugger, close that window, bring back my Test Suite and just fix this. So really, I want it to be, if it's more than 5, let's see, does that here, there, there we go.
[00:16:52]
If it's more than 5, then it does that. Otherwise, if it's less than or equal to 5, it increments by 3. Okay, so that fixed those tests. Now, we have the last one, should drop quality to 0 when sellIn has passed. I'm just gonna add another else case here.
[00:17:11]
I'm just gonna say item.quality is 0. Sorry, I'm going to change this so that if it's greater than 0.
>> Francesca Sadikin: There we go. So that I had another incorrect conditional right there. So now, all of my tests are passing, cool, but we have a whole other set of code here to work.
[00:17:37]
We have the default items we still need to handle. So I want that code to be in this else block. And so what I'm going to do is comment everything out in here, and I click Save. Everything else is passing except now the default items that we need to make pass.
[00:17:58]
So let's just start. We need to have sellIn decrement by 1. So let's just do that, sellIn decrements by 1, okay? The next one is that quality,
>> Francesca Sadikin: Should have a quality value that also decreases by 1. So item.quality also decreases by 1, and then this one's should degrade quality twice as fast when sellIn has passed.
[00:18:33]
So I'll just say if item.sellIn is greater than 0, we decrement. Otherwise, item.quality decrements by 2.
>> Francesca Sadikin: And then we just have the last one. It should never have a negative quality value. So I'm just going to write a little check here that just says, if item.quality is less than 0, we set it back to 0, okay?
[00:19:12]
So we have this all passing, I'm going to just slightly close this over now. So because we have all of the requirements given to us and we were 100% sure that the requirements were there, we have written the test, I'm going to delete this code. Please don't delete code in your future companies just like this easily, because [LAUGH] you wanna be 100% sure you understood it.
[00:19:39]
And other engineers confirmed that you understood it before you just delete a bunch of code like that. So just because this is a very scoped project, I felt comfortable doing that.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops