4.6 Get more context: clean up and test your code - Video Tutorials & Practice Problems
Video duration:
17m
Play a video:
<v ->Okay, so we've looked quite a bit about how to write code,</v> but we haven't looked yet about how to clean your code up and make it nice and readable and more robust. So, that's what we're gonna look at in this lesson. And as we mentioned earlier, the creative Python Guido, one of his mantras was that code is more often read than written. So we wanna make nice readable code as much as possible. So that bring us into these two concepts of code smells and refactoring. So a code smell is something that you see in your code where you're like, hm, this smells a little funny. Maybe it could be tidied up a little bit. Maybe there's something I can do to make it smell nicer. And then refactoring is that process of tidying things up, and keeping the functionality the same, but changing the way that it's implemented so it is tidier. So, here's a list of some code smells you might encounter. Maybe some confusing names. You read a variable, or a method name, or a function name, and you're like, hm, I don't really know what that's supposed to do. It's not very intuitive. You might see some unused code. You don't often just write things for the heck of it, but maybe you've changed something, and then something further up isn't needed anymore, and someone forgot to take it out. You could also have some duplicated code. So we saw that in the number guessing game when we deleted a lot of duplicate code and just put it inside of a loop instead. There's also unnecessary comments. Or no, so there's also first long methods or functions. So if your functions are doing a whole bunch of stuff, maybe it'll be more readable to extract certain things and encapsulate them in a different function, and you can give it a specific name. So like if you've got a recipe for making sushi, you can like extract a part and just call it, make rice. You don't have to include every step to make the rice inside the entire function to make sushi. And then also unnecessary comments. So comments are, it's good, especially if you're learning. And it's good to help you remember things, and we'll look at what is a good comment or a bad comment, but sometimes comments aren't necessary. And sometimes you change the code, and the comments don't even reflect that change. So they're not just unnecessary, they're actually misleading. And then there's also something called high cyclomatic complexity. That's mostly if you have a bunch of like if statements, or for loops, and there are a lot of like different pathways that this code could go into. It's harder to test if you've got like a super complex piece of code with lots of different pathways. And how can you refactor things? So, you can rename variables, functions, methods, classes when we eventually get to classes. You can extract some pieces of code. You can extract code into its own function. You can break up like a large class into two smaller classes once you know what classes are. And you can also delete code if it's just unnecessary. Deleting code is one of the most satisfying things to do when you're working in a large code base. So here's just a very simple example. I've got this piece of code. Now, what can I do to it? So, one thing is it's got kind of a confusing name. And to refactor it, I can rename it. So, it used to be called my function, but look at what it actually does. It returns one number plus another number. So we can change it to be called add, and then that's renaming the function. So that's nice and simple, great. We've also got some unnecessary comments. And in that case, we can just delete that code. So we have this comment here. It is now unnecessary because the function name just says what it should be doing. And so we can just delete it. And then we also have some unused code here. And so in that case, we can also delete the code. So we've got this my number. Maybe it was used previously for something else, but we're not using it anymore. So let's just delete it. So those are three like simple refactorings you can do. We had a very simple example of an unnecessary comment, but when are comments necessary? What are good comments? So ideally, it should tell you something that you need to know about the code that isn't obvious. So maybe instead of telling you what the code's doing, you can use a comment to say why you're doing this. The person reading your code should be able to follow along and see what is happening, but they won't know your logic behind it. You could also use comments to save future developers some time. For example, if you're following some specifications that are described somewhere else, you can provide a link to that, or you can add that to the comments. And then when you're refactoring, how do you know if you're breaking stuff? How do you know that the functionality is staying the same? Well, that's why you test it first. If we were to say test this piece of code, it's very simple. We might have something that looks like this. We might ensure that if we add and pass in the numbers zero and zero, that the result of it is zero. These are all test cases that you can use. In general, you want to test like a variety of different cases. You wouldn't wanna just have too many tests that are testing a very similar kind of thing. So here we're testing zeros, positive numbers, and negative numbers. So the way that it's implemented, passing in some strings will concatenate that string. Passing in some lists will create, return a new list with the lists combined. And then what happens if you pass in none as a value? Well that'll return an error. What if you don't pass in enough values? Those will also return errors. If you pass in too many values, those will also return errors. Good tests coverage, writing good tests also means testing these like, they would be called like sad pass as opposed to the happy pass. And knowing what good tests are are like part of being able to be a good developer. So why would you test your code? Well we wanna have confidence that there aren't that many big bugs. Code often has bugs. It's gonna be hard to ensure that there are completely zero bugs in there, but at least we have some assurance that the big ones are caught. If we wanna refactor things, we wanna make sure that we're not breaking functionality. So it's more likely if you've got well tested code that when you come back to it, or when someone new comes to it, that they feel confident in changing things up. And then people can clean as they're adding new functionality. And it prevents spaghetti code, what's like known as just like a big jumble of code. The code that we wrote for our word guess game is all just like, do this, then do this, then do this, and it's maybe a bit of a jumble. There are different kinds of tests. There's actually a whole world and language around texting. You don't have to worry about that much right now. What we've been doing are just manual tests so far. If you are relying on manual tests, ideally you would have a test plan so that if you do change anything then you can go through that plan and make sure that everything is covered. There's also something called unit tests, and they test small components like functions. So the test that we have here would all be like unit tests. They're testing a small like just a single function. There's also something called an integration test, and it tests like multiple functions or how some different components from different parts of the code interact together. And then there is a whole bunch of more ones that you can look up on your own. If you're working at a company that has like a larger code base, you're gonna have at least one, or two, or more of these other tests that are involved. And if you're curious, here are some testing libraries to get you started. I'm gonna show you a very simple way of testing using the key word assert, but besides that, there are lots of more libraries that help you write more advanced tests. There's like standard unit testing frameworks include unittest, which is built in to the standard library, as well as pytests, which you would have to download. There's something called mocking, which is useful for writing tests that involve components outside of just the thing you're testing. For example, if you wanna test that this thing actually printed to the console then you could use the mock library to test that. And then also our source code, you can actually run static file analysis on your source code to make sure that it's pep8 compliant and what not. So that pep8 is one and Pylint is another library that you can use for source code testing. Let's come back to our word guess problem and see if there are obvious things that we can do to refactor this. So one is that we've got some comments in here still. And I'm just gonna delete those because some of them, it's pretty clear what's happening, and then some of them it's not. So, here, check if they won. So I can actually extract this into a function. So I'm gonna cut all of this and add a new function here. Call it, did user win. And there are some things that I've got here that I need. So I can ask the user to pass in an answer and the guessed letters. So those are what are needed to be able to run this function. And then we wanna also return won when we're done. And one thing that we can do as well, and maybe it would be better to write some tests first before we change this. So let's keep it like this, and then we can use this did user win. Did user win. And then pass in the answer and the guessed letters. So now, check if they won is pretty self evident from that name. And I'll just refactor this a little bit to make it clearer. So in PyCharm, this would be shift + F6 to rename something. And we'll call it user did win, and then it's saying that it found a reference to it. So I'll refactor that. And now it changed this. So that's good. And let's write a couple tests for this. So, I can use this assert function, and it just accepts a true or false Boolean, but these can be comparisons. So we'll look at this first. If it's true, and I run this program, it'll just go, okay, great. Let's continue on. But if this is false, then I run it, then it fails and say that there's an assertion error, and it found a false. So that's good. We can use this as very simple like in file tests to test some functions. But it's not the best testing strategy in the long run. So I can say it did, user win. User did win. And say the answer was hello. And the guessed letters were h, e. Oh, that's too long. Let's, we get H-I. Okay, let's run that. And that continued on. That's good. And then if it was just i, s, this will be false. And we actually wanna check that not. So I can do this because user did win returns a Boolean. Otherwise, like I could write something like this. Like equals true. And this equals false if I remove this not. Okay, so that's good. Everything's working. And now, I can work on refactoring this function, and I can get rid of this variable because once it gets in here, then it wants to return false and break. And it just wants to return this value. So I can return false right away if it gets into this case, if the letter's not in the guessed letter, guessed letters. And I can return true, oops, if it never reached here. So, let's run this again. And the code runs without breaking. And that's good. So if I, like say I forgot to use a return value here, then I've got an error because this did not return true. Okay, so there are a lot, there are a few more refactorings we can do here. I've included in the challenges and the example solutions this number, no, word guessed refactored, and I've also put some tests in a separate file so you can not muddy up this file with a bunch of tests. You can just run the test and file separately. It's also got some validation in there. So in case the user inserts, like types in something that's not just a single letter, it'll check for that as well. So those are all in the solutions. So, yeah, that's, check that out, and that's how you refactor things.