How To Write Unit Tests in Python • Pytest Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Hi everyone, welcome to this video where I'll be sharing with you how to write unit tests in Python using the PyTest framework. If you don't know what a unit test is then you can think of them as essentially functions that you can write that when you run them it checks whether or not your code or the rest of your application is working in the way that you want it to. If your application is working correctly then these tests should pass but if the application has a bug in it or if the code hasn't been implemented properly then we can expect these tests to fail. So by running the tests, it's a good and fast way to automatically check the entirety of your application and if everything is working the way it should. And this is a very useful and powerful thing to have at your fingertip because as projects become really large, it becomes difficult and tedious to manually go and validate that each of those things that were working are still working after you add new code or make changes to the project. So things like refactoring can become really risky if you don't have unit tests because you don't have a fast and easy way to check that the code still works exactly the same way that it does. However, if your project does have unit tests, then at the touch of a button, you can automatically validate whether the project still works and that new functionality has been implemented correctly, but also that it doesn't break existing parts of the code that was working before. Overall, unit testing is one of those skills that I really recommend investing time into learning because it is just a skill that will take your software engineering to the next level. In this project, we'll be writing some unit tests to test a simple shopping cart application that will also create as part of this project. And the unit test will do things like check whether or not we can add items to a cart, check that the cart throws an error if we try to add items when it's full and check that we can correctly calculate the total price of all the items in the cart. And to do this, I'll be using the PyTest framework, which we will have to install since it's not part of the Python standard library. I do want to mention, though, that Python does ship with a standard library for unit testing, and it's called "unittest", but the reason we're not using this library is that I believe that PyTest is a far better experience. It's far easier to use, and it's also way more popular than the standard library that Python ships with. So those are my reasons for using PyTest. But for what it's worth, most of the skills that we'll be covering in this tutorial, such as assertions, test cases, mocking, and fixtures – these are concepts that transcend the PyTest framework and can actually be adapted or generalized to any unit testing framework in any language, including JavaScript and C#. That's pretty much it for the introduction. Let's go right ahead and get started with the project. To get started with this tutorial, we first have to make sure that pytest is installed in our Python environment. So you can do that by just typing `pip install pytest`. And that should install it into whatever environment you have set up. And in order to test if the installation works, you should be able to type `pytest --version`. So if this command runs, you should see a version number and mine is 6.2.1. So now the pytest is installed, we're ready to get started. I've created an empty directory called `unit-test-example`, but you can do this in any project directory you want. And I'm gonna open up this directory in my editor, which is VSCode. So now I'm in my project directory in my editor, and it's just an empty directory. To write a unit test, we first need something to test. So I'm gonna write that something to test, which is basically gonna be a shopping cart script. And I'm going to copy some code that I prepared earlier just to create some interface for it. So this shopping cart is going to be a class. And we're going to be able to add items to it, get the size of all the items in the cart, get a list of all the items, and get the total price. But because the item itself doesn't have the price attached when we add it to the cart, we're going to need to pass in a price map to look up the total price. So that is why we have this price map as part of the interface. Okay, so this is basically the application that we wanna test. How do we actually write a unit test for this? Well, the first thing to understand is that to run a unit test inside a directory, we go to that directory, which I've already opened here. Let me clear that. And we simply just type `pytest`. And that will run all the unit tests it can find in this directory. But when I ran this, it says no test ran. So it didn't find any test. So how does Pytest find or discover test? It's actually really simple. It does this by naming convention. There's a lot more settings you can tweak behind the scenes, but out of the box, Pytest will discover tests in your project by the way you name them. So a shopping cart doesn't look like a test file, so it's not gonna detect that. However, if we create a file that starts with "test" and then an underscore, that will be detected as a unit test. So I'm gonna create a file here called `test_shopping_cart.py`. This also works if the underscore test comes at the end of the class name, but I like to put it ahead of the class name. And usually I also like to have a parity between classes I have as functional code and my tests so that people who are working on this project can find the unit test quite easily by just looking for the same file name the word test in front of it. But now that we have this, let's actually run it again and see if anything changes. Running "pytest" again, of course, still no test ran. That's because even though we created the file, we don't actually have the test function yet. So I can start writing individual unit test function like this. And I'll call this one "can_add_item_to_cart". So that's going to be my first unit test. And it's not going to do anything, it's just going to pass. And with PyTest, the important thing to keep in mind is that each unit test is basically just a function. So each function that passes is a unit test that passes. So let me run this again. Again, still no test ran. And that's because even though this function is in this test shopping cart file, the function name itself isn't discovered by PyTest because it's not named in the same convention. So even the functions need to have this naming convention with the "test" as part of the name. So if I call this function now, "test_can_add_item_to_cart" and I run it, just run PyTest, it will discover that test and run it. Now I have a single unit test that is passing and it's usually good to see a passing test, but in this case we want the test to fail because it needs to check that I can add the item to the cart and it's not really doing that. It should fail because I haven't implemented any of these functions yet. So the first thing I want to do is actually write some code here that will fail this test when I try to add something to the cart. Let's see how we can do that. So first, because this is a class, we need to import it and then create an instance of it. And then call this function to add an item. And my IDE has imported this automatically for me but if it didn't for you then be sure to type in this first import line as well so that we have this shopping cart class accessible from this test. And then I'm gonna call "cart_add" and the item is a string. So I'm just going to write an "apple". So this card will just have an "apple" there. Now, how can I check that I've successfully added "apple" to this card? Well, I've got a bunch of other functions that I can use as well to check its behavior. So I can check that the size equals to 1, for example. So cart size should equal to 1. Now, how do I make this fail this test if this is not true? There is a keyword in Python we can use which is "assert" and that basically checks if the statement is true. If the statement is "true" this line will succeed. If the statement is "false" then this will throw an exception and if an exception is thrown then this test will fail. So let's go ahead and try that. Okay, so now the test fails, and it fails because 0 is not equal to 1. Because in this function you can see that it actually returns 0 here, but we expected it to be 1 after we added "apple". So now this is the stage where we want to actually implement this functionality in the shopping cart so that it can pass. So what do we need to do for that? We are adding an item, So let's actually keep, maybe keep a list of items in the shopping cart itself. This can be a list of strings. And then when I add it, I'll just simply append this to that list. And now to finish off the function, I also need to return the size of the list instead of just zero. Okay, so I've implemented these three. I haven't implemented these two yet, but that's fine because I'm not testing them. And if I run the test again, my test should now pass. And this is pretty much the development cycle if you are writing functionality for a large project and you want to write test and functionality at a time. You don't have to implement your whole class at once, or all the tests at once. It's usually good to just build this out slowly and test piece by piece as you're building it out. I find it's quite easier that way. Now, if we want to write another test, then it's pretty easy. Another "test" in this case is basically just another function. So I can say "test when item added, then cart contains item". Okay, which is a bit of a mouthful for a name, but I think that descriptive names for tests are usually better than names that aren't descriptive enough. So we're gonna copy this code again, create this shopping cart, add this apple, And this time, instead of asserting that the sum is 1, we're not going to do that again because we already covered that in this test. Instead, we're going to assert that if we get the items, that this "apple" is inside this. Okay, so now this test is ready. If I run this, again, because I haven't implemented this "get items" function, this should fail. I usually like to, before making the test pass, I usually like to run them and make sure that they fail just to have that validation that the test actually fails when the condition isn't met. So here it fails, which is good because we wanted it to fail. Now let's make it pass. And now if I run it again, we have two tests passing. So far, so good. Now, another interesting use case for unit tests is to test that our application throws an exception or raises an exception if we try to do some behavior. So for example, in this case, let's imagine that we actually have a max size for the shopping cart. So we can't let the user add more items than this max size. And if they try to do that, I want it to fail. So how do we test that functionality? As you saw earlier, any kind of exception that is thrown or raised inside a test will cause the test to fail. So let's go ahead and actually write this test first and see what that looks like. "Test when add more than max items should fail". Okay, so this is kind of what we wanted to test to do. We're going to have to create a new shopping cart here. And now the shopping cart actually takes a "max size" argument. So we're going to have to put in some number for this as well. So here Here I'll put in 5. And if I add more than 5 items, actually I'll try to add 6 items. If I try to add 6 items, I actually want this to fail and throw an exception. So how do I do this? This passes because we haven't implemented it. Do I make it fail if it doesn't throw an exception? So if you're new to unit testing then it's important to know that most test frameworks, including PyTest, has a way to check whether a failure is thrown or not and then pass the test if it fails, if that makes sense. I know it's a little confusing but once you see the example it will start to make sense. So we just go to this getting started page and then looking at some of the examples here. One of the very first examples is that we can "assert" a certain exception is raised. And this is such a common use case for unit tests that it appears so near to the top of the getting started guide. So looking at the example we just have to add this line with pytest. "Raises" and then we can put the type of exception that we want here and then we put our function. So let me copy that and put it into our code. So let's look at this it says "with pytest raises system exit" so first of all it's orange here because I haven't imported pytest yet so let's go ahead and do that. Okay, so now this "with pytest raises system exit" basically means that whatever I run under this line I expect it to throw this error and if it does throw this error then this test will pass if it doesn't throw this error or if throws a a different error than this test should fail. However, because I'm adding items to a cart and I want it to fail when I add too many, I don't want a "system exit" error. I want a different type of error. If I don't know what I want yet, I could just do a generic error like this. But actually, I think something that would be good here is an "overflow error". So I'm just going to use that as my error type. So now I want this to raise an overflow error. But it's not doing that because I haven't added the condition yet. So if I run this test again, I should expect it to fail. And here we see the failure and the reason is that it did not raise the overflow error. So let's go ahead and implement that. I'm gonna start by storing a reference to max size. And then I'm going to check if the length of the items is actually, I'm going to, I already have a function for size, so I'm going to check if the size is equal to max size. And if it is, I can't add any more items. So I'm going to raise an overflow error. Otherwise, I will continue adding this item. So let me run that again. And now it passes because it does raise the overflow error. Now I want to take a moment and just analyze the way we've written this test because I want to show you some common mistakes that you can make when you write this test. So even though this does pass in this case, and this test sort of looks correct, there is a problem here because we actually put this with "raises overflow error" on top of this whole range operation. So we actually don't check that it raises it on the sixth apple that we add. We will be satisfied if any of these additions raises overflow error. Why is that a problem? Because I could actually have a bug in my code that the test would not catch. For example, if I make size equals 1 instead of max size, or I do something like max size minus 1, this is incorrect because it's going to not add the items that I want. So in this case, if max size is 5, max size minus 1 would be 4, and it's going to fail at 4. And if I run the test again, the test is still going to pass, so it doesn't catch that bug. So when you write your test, you have to be really careful of different ways that it can pass, but that will let bugs and bad functionality go through. How can we fix it in this case? Well, I can just pre-fill the cart with five items. Or I can make the limit 1. But I want to keep it at five for now. So I want to pre-fill it with 5 items, and then I want to add the 6th item and only assert that this raises on the 6th item. So now if I run this, because I'm only checking it for items, it should fail. And then I should be able to fix it by fixing that bug. So let's see if that really works. Okay, so now it fails. We hit an overflow, cannot add more items before we entered that "raise" condition. So let's go ahead and fix that bug take this minus one out and run it again and now it passes and our test is a little bit better because it can catch more failures. Now let's go ahead and implement the final test for this "getTotalPrice" method that we have and I'm going to show you something new as well regarding dependencies we pass in like this price map here. But first of all let's just start by writing the tests. So I've got my tests here. Before we actually go ahead implementing this test I just want to mention that okay if we have four tests already or even if we have multiple files of tests it can get really long to run the test or maybe you just don't want to run all the tests and you just want to work on the one that you're developing. With PyTest there is actually a way to scope directly to just run this test on its own. So let me just print out a statement here and say "testing". Okay so now I just want to run this and none of the other tests. If I just run PyTest on its own you can see that it runs for test, but if this is a test I'm working on and developing I might just want to run that specific one. To do that we can just write the name of the file that we want to test. Or more specifically the path to the file. But because this is a flat directory, the name is right here I can just write the name so it will look like this. Pytest "test shopping cart" and that will test the file. If you have multiple files, that will test that specific file but I want to test a specific function. So I do that by doing a double colon and then the name of the function which is "test can get total price" and if I run that now you can see that the test will pass. But I don't know if you notice something strange here. It's run the test but this print statement I've added isn't showing up. So this is actually something that's confused me for a while. I think that that pytest only shows the "print" on some conditions, for example, if the test fails. I'm not quite sure. But if you want to show all of the prints whenever you run the test, you can put in a flag "-s", and that will cause pytest to force all these statements to print out. So now I can see this "test can get price" line that I printed out from my test. Okay, so back to this last test, let's go ahead and implement that we can get the total price. And this is a little bit more complex because we actually need to pass in something called a "price map" here, which it doesn't have a type. And I'm going to explain that later. But for now, just know that the cart itself doesn't know the price of these items. And it needs a third party information in order to get that. Let's first start by creating the cart as usual, Shopping cart five. And then I'm gonna add two items to it. I'm gonna add an apple and an orange. And I want a price map. I want the price map to be a dictionary that contains the keys of these items. For example, "apple", and then I want it to have a number. I'm gonna make that a "float", which is the price of this item. And now I'm gonna write my assertion, which is "cart get total price", and I have to pass in the price map, so I'll pass this in. And I need to check that the total price of these items should be three. So let's see if this test will fail, which I expect it to because this doesn't work yet. And as expected, it fails because "None" is not equal to 3. So, let's go ahead and implement that. Okay, so the test passes and that's good. You may have noticed that even though price map is a dictionary, I decided to use the "get" method, which is kind of unusual because when people use dictionaries in Python, we usually get items like that. But dictionaries also have a get method that we can use that returns the value of that key as well. There's a reason why I use this instead of the index method, but I'll show you that later. Before we get to that though, I just wanna review where we are now. We now have a project with a shopping cart application and a unit test file that we can run with pytest and there's 4 unit tests inside this file and it correctly checks whether our application works. And if we insert a bug into this or we make up a mistake with the application, we can count on our test failing to tell us that, "hey, something's gone wrong" and we need to fix it. So that's where we are. But if we wanna scale this and this gets really big, then we need to look at ways to reduce duplicate code. And one of the duplicate codes that we have right now is when we actually set up this shopping cart. Thankfully this is only a one-liner setup so it's not that bad, but it's very usual for tests that have complex applications to have several lines of setup. For example, maybe 5 or 10 lines, that's just to set up a server or a client or something like that. In this case, the setup is very simple, but we still want to do this once. For example, if I want to change my shopping cart size to 3, right now I have to do it across all of these things, and that might not be ideal. So with PyTest, there is actually a way to set something up once in a setup function and then pass it to these tests so that we can use the pre-setup cart or item or, you know, context of the things we wanna test. And this is called "fixtures" in PyTest. So if you type in "fixtures" in the documentation, there should be an example here. And the description is that "software test fixtures initialize test functions". They provide a fixed baseline so that tests execute reliably produce consistent repeatable results. So if we look at the example for how to use it... Tthis fruit example here, we have to add this annotation "@pytest.fixture" in front of a function, and then we can use that function as an argument in our unit test and then we can use more than one as well, like this. So let's say that I want to use this fixture now to initialize my shopping cart once and let's imagine that the shopping cart initialization becomes more complex than this one liner. Then it saves us having to redo all that work in all our different tests. So let's try that. And I'm going to call this fixture "cart" because that's going to be the name that we use inside these tests and it's going to be a new shopping cart. Actually, I'm just going to return a new shopping cart with a capacity of 5. So that's just going to be my fixture. And now I don't need to reinitialize this cart each time on its own anymore. I'll just pass in this fixture as an argument to the test. So that's pretty good. We now reduced the setup and the boilerplate overhead in each of the tests and have it that sort of organized into this fixture function instead. And this could be a really long function. For example, if we need to do database connections or anything to set up this cart that we want to test, but now we just have to do it in one place and then we can use it in all of our tests. In case you're wondering, this fixture is run new for every time we run a unit test. So each of these carts are created new every time each test is run. So it doesn't reuse the cart that it created before. So you can be sure that whatever it's testing with is exactly what is returned here without any of the other tests modifying that object. Now for the last thing I'm gonna show you is how to mock dependencies. That means basically if your test relies on something else to work, for example, a database connection, an API call, or something that you don't control, then you want to fake its behavior, or actually even a common example of that is a random function. Usually we write code, especially in games or many types of applications have some kind of "random()" function. And we want to test when something really rare occurs. For example, maybe 5% chance from a "random" function. We can actually mock the "random" package to force it to return the number that we want instead of just running tests a lot of times and hoping that we get the right distribution. And this is also very common, especially for things like API calls or even if you have classes in your project that you control, but you don't want to include the full class logic inside a unit test. That's when you refer to the "mocking" technique, which is kind of creating fake behavior for something so that our unit test can use it and test the shopping cart. Now this is why I have this price map here earlier and then I have this get function here. It doesn't know what price map is in this function but it just knows that it has a "get()" method and that it can get the price for an item. Now with that in mind, I have just created a mock for it now as a dictionary. But if this was a real big application, I'm likely to have a price map object or a price database object like this. So for example, let's call this "item database" as my price map object. And this could be a class as well. So imagine that I actually have a class in my project called "item database". And it could be a very complex class that makes connections to an actual database to fetch the price of an item. But we know that this class will have a "get()" function. And it's going to have get an item. So we know that it has the ability to find the price, but we don't know anything else beyond that. This could be a very large class to have. Alternatively, we might be wanting to work on the shopping cart while somebody else in our team implements the database. So we can't wait for this to finish implementation because that's gonna take too long. We wanna be able to start developing the shopping cart before this is actually implemented. However, we can rely on the fact that it will have this function and that this function will work. So how do we solve this problem? First of all, let's start by actually replacing this price map with an instance of our item database. So I've actually used a real instance of this item database I've created now in my test and I pass it through, but this should fail because item database is not implemented yet. Let's see what happens. Okay, "unsupported operand". Yeah, this doesn't work because this one is failing. Instead of waiting for my team to implement this or instead of relying on the database connection for this to work, or actually another good point is that we might have the database updating its item prices independent of this test. Or even worse, our test could suddenly fail even though we didn't change anything about the cart, but the database behavior itself changed. In order to solve all of those problems that I mentioned, so having to rely on this, having to rely on connections, having this behavior change underneath our tests, we can "mock" the behavior of this. And we do that by using a library called "unittest mock". So this is actually a standard Python library. So if you type in "unittest mock" in the documentation, you should find this page here. And then we can look at some of these examples. It'll help you better understand. Here's a really simple example. We create a "mock" by just assigning the method that we want to be equal to a "mock" instance, and then we can force it to have different return values like that. So let's go ahead and give that a go and see what it looks like. So now I have my item database. I want to mock the "get()" method because I know that this method is gonna exist, but I know that it hasn't been implemented yet. I'm gonna make it a "mock" method and I'm gonna set its return value to "1.0" So now I've mocked this method. Now when my cart uses it, whenever it calls this get, it should receive "1.0" as the output. So let's try and run that and see what happens. It should still fail though, because it will return 1.0 twice, but it's not gonna equal to 3 because 1 + 1 is 2. So let's see if it does fail with that error instead of the previous one. And here we go, it is now two is not equal to three, so the assertion fails as expected. Just to show you, if I actually add a third item to the cart, because each of these are priced at one, the test should now pass. Okay, so our mock is working the way we want it to, but we didn't want to add three items, we only wanted to add two items and have the price equal to three. And we wanted each of these items to have different prices. So how can we do that if we only have a return value? Well, we can actually customize the mocking behavior a little bit more if we wanted to return or have a custom function here by using the "side_effect" argument instead. And let's go back to the documentation here. And there's a lot of stuff about "side_effects". So we can set the "side_effect" and the "side_effect" is gonna be a function. So it's gonna take whatever argument the mock receives and the mock will receive whatever return value the "side_effect" receives. So with the "side_effect", we can do all sorts of things, like throw an error, do something else to a different class, or in our case, we want to return different values depending on what the input is. So let's create our mock "side_effect" first. I'm gonna create a function here, like a local function in the scope. And we are mocking this "get()" function, so I could just copy this interface, that's essentially what we want. I want to mock a "get item", and I want to return 1 if it's apple, and 2 if it's orange. So I could do it like this. And let's just put that into the side_effect instead. Now, I think that this should pass. Let's try it out. And there we go. We have successfully mocked the behavior of our item database without having it to be implemented. And that pretty much wraps up our unit test tutorial. We now have a unit test here and a shopping cart that we're testing. And we've written four different tests to make sure that it's doing what we want it to. And we learned also how to create a test fixture so that we can set up the test once for each of these things without having to duplicate this code. We also learned how to assert that certain exceptions are thrown. And we also learned how to mock dependencies in our tests so that we can force the behavior in dependencies and things that are out of our control. And that's pretty much it for the tutorial. Even though this was quite short and only covers a fraction of what PyTest can do, I find that 80% to 90% of my day-to-day use cases on large projects are pretty much covered by all of the techniques that I've mentioned here. So even though there's a lot more depth to this, this will probably get you quite far already. Anyways, I hope you enjoyed the video. I hope you found it useful and thank you for watching.
Info
Channel: pixegami
Views: 99,864
Rating: undefined out of 5
Keywords: python, testing, coding, unit testing, pytest, tutorial, software, developer, software dev, tech, pytest unit, unit test pytest, pytest tutorial, learn pytest, how to pytest, how to unit test, unit test in python, best way to unit test, how to test python, how to use pytest, learn python testing, learn unit testing, testing tutorial, python pytest tutorial, python testing tutorial, testing python code, pytest code, beginner
Id: YbpKMIUjvK8
Channel Id: undefined
Length: 35min 34sec (2134 seconds)
Published: Sat Apr 09 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.