Getting Started Unit Testing with Pytest — PART 2 | by Ben Bogart | Towards Data Science

Last week I wrote about getting started with unit tests. We covered basic assertion testing, parameterization, fixtures, and most importantly, how tests save you time so you can get more done or spend more time with your family — your choice. This article starts where that one left off, so if you need a refresher, here is that article:
towardsdatascience.com
One important principle of unit tests is that they should be independent. In the context of testing, this means that our test should only be dependent on the function we are testing and not any other services or functions. Immagine your function makes an API call to goggles.com and returns the most relevant swimming goggles for your search using some complex AI. If we don’t mock that API call, our tests will be dependent on it.
There are several problems with depending on that API call in our tests. The one that gets brought up most often is time. An API call can take a second or two, and we want our tests to be fast. W also want to test often, and if we are getting charged for those API calls, testing could get expensive. But neither of these is the main reason we don’t want to depend on that API.
Remember, the main purpose of unit testing is to save us time in maintaining our project. If that API goes down, our system may break, but when I run my tests, we don’t want the test to tell us that the problem is the function that calls the API because it's not. We want our tests to tell us how our system will respond if the API goes down, and we can simulate that by mocking too. Good tests will tell us if our code works independent of any other code or services we are connecting to.
The main idea of testing is to tell us if our code is doing what we expect. If goggles’ code breaks, that should not affect our tests. It should be reflected in their tests.
Ok, with that out of the way, what do we do about it?
To follow along, you will need to install pytest, and pytest-mock.
pip install pytest pytest-mock
You can also follow along in the GitHub repository for this article.
To get started, we’ll pretend to make an API call.
from time import sleepdef fetch_goggles():    # make an api call to goggles.com    # ...    sleep(4)    response =  {"foo": True}    return response
And we will write a test for it just as we did in part 1 of this series.
def test_fetch_goggles():        import project    result = project.fetch_goggles()    # test that the value in result['foo'] is True    assert result['foo']
Run the test.
Wonderful. Our test passed. But what if the API call had failed? We can simulate that by changing the return value of our function. If you are following along with the code in the GitHub repository, you can uncomment the new response line.
def fetch_goggles():    # make an api call to goggles.com    # ...    sleep(4)    # return a 408 (timeout) status code     response =  {"status": 408}    return response
Oh no! We can see that our test failed because the result object did not have foo as a key.
If this API call actually failed, it should not have broken our code. So how do we ensure it doesn’t?
This problem can be solved using the mocker fixture and “patching” the API call. In its most basic form, there are two elements we must add to our test function to patch with mocker.
Add mocker to our test function definition as a fixture (fixtures were discussed in part 1 in case you missed it). The mocker fixture is provided by pytest.“Patch” the function we want to circumvent
Add mocker to our test function definition as a fixture (fixtures were discussed in part 1 in case you missed it). The mocker fixture is provided by pytest.
“Patch” the function we want to circumvent
The new code looks like this:
def test_fetch_goggles(mocker):    import project    mocker.patch('project.fetch_goggles',                  return_value={'status':200,"foo": True})    result = project.fetch_goggles('Hi')    assert result['foo']
Let’s take a closer look at that mocker.patch line. The first element is a string matching the module path to the object we wish to replace. Notice it is the same as project.fetch_goggles on the next line but without the (). I also passed in a variable to our project.fetch_goggles function to illustrate that the return value will be the same no matter what you pass to the function. A mocked function will accept anything you give it without throwing an error. This is an important point and a place to be careful as you get more familiar with mocking.
Running the test:
Bam. The test was completed in 0.02 seconds. You may not have been able to feel it from the pictures, but 4 seconds felt like a very long time to wait for testing to complete. This is much better.
Another opportunity provided by the mocker that we definitely don’t want to miss is the ability to test the failure of our API call. After all, we do want to handle failure with grace in both life and code.
def test_fetch_goggles_failure(mocker):    import project    mocker.patch('project.fetch_goggles',                   return_value={'status':408})        result = project.fetch_goggles()    if result['status'] != 200:        # recover gracefully        graceful_recovery = True            # assert that this was not a success    assert graceful_recovery
We’ve changed the return value in our mocker.patch to {'status':408}. This simulates the API call timing out. Now we can test how we recover from an API call failure. This makes our code more resilient and still does not depend on actually calling the API.
Now we know we are graceful.
If you haven’t heard this yet, you will. You will hear it many times before it makes sense. That is not entirely your fault. Let's see if we can break it down.
First of all, “where to mock” is confusing. What everyone who says this means is “how to reference the object you want to mock.” You actually write the mocker in the test function. The issue is that the path to the object you are mocking needs to follow a specific route.
You may have experienced this. You import pandas and write some code. You decide to break some of your code out into a separate helpers.py file. You import your functions into your application from helpers.py, but when you first run your code, you get an error: NameError: name ‘pd’ is not defined. Clearly, you forgot to import Pandas in your new helpers.py file. This is the key to understanding how to create the path to the object you wish to mock.
We will create a helpers.py file with our API call function:
# helpers.pydef fetch_goggles():    # make an api call to goggles.com    # ...    response =  {'status':200,                 "count": 5}    return response
We will also create a new function in project.py where we make use of this function.
import helpersdef count_goggles():        result = helpers.fetch_goggles()    return result['count']
Finally, we create our test function.
import projectojectdef test_count_goggles(mocker):    mocker.patch('project.helpers.fetch_goggles',                   return_value={'status':200, "count": 4})    result = project.count_goggles()    assert result == 4
To patch our function, we follow the path from where we are to the function. From the test file, we imported project. From project we imported helpers. fetch_goggles resides in helpers. Thus we get project.helpers.fetch_goggles as the path to the object we wish to patch.
Mocking is a handy tool in the testers toolbelt, and since you are now a tester, you will be mocking. The value of mocking is that it allows us to keep our test truly independent and gives us a way to test for different types of return values without needing all of the context for those values. As you are learning to build better unit tests, always remember to think through the independence of your unit tests.
There always seems to be a lot of discussion around “where to mock,” and it's probably the framing of this question that causes the confusion. You write the mock in your test function and follow the module path to the object you are mocking. I recommend not thinking about “where you are mocking” because that's just confusing.
Now go make a mockery of your unit tests.