Patch Your Dependencies Like a Boss

Meysam
Geek Culture
Published in
9 min readApr 25, 2021

--

Chess pawn with a king crown 👑
Photo by Pixabay from Pexels

What is an automation test & why you need it?

Whenever you are writing a python application, whether it is a web app, a library, a machine-learning algorithm, etc. you’re gonna end up testing your code and I’m not talking about manual tests buddy; we’re doing it automation test style yo!

Automation tests make future changes more manageable, in terms of what has broken so far. It also makes clear documentation of your code right out of the box. If you’re not satisfied with why the automation test is good for you, check this link and the links I am providing you at the end of this article.

Now that we’re on the same page that automation tests are awesome (we are, aren’t we?), let’s delve into the more important stuff. One of which is how to patch the dependencies in a unit test.

Unit test is the first step of your automation tests and there are some arguments regarding “whether or not we should skip them and jump to integration test” but that is not the main topic of this article and you’re free to figure that out on your own using the links provided at the bottom of this page.

A young male sitting in front of laptop and focusing
Photo by Wes Hicks on Unsplash

What is “patching” then?đŸ€”

To share a little bit of a personal experience, I have to say that patching dependencies have been my main concern for a long time, and now that I have finally found an elegant way to approach this problem, I felt really excited; the main reason I started writing about it here.

Now to get a feeling of what we are trying to solve here, imagine you have a service that receives input from the user, does some external API calls & after doing some processing, returns the ultimate result.

In this scenario, to test your application, that external API call has to be available every time your tests are running either locally or in your pipeline.

But what if your internet connection is too goddamn slow? Or what if the machine running your tests does not have internet access? There is also the possibility that the target website might just be down and inaccessible.

The same example applies to interacting with a database. It just might be too costly to set up a database server every time you need to test your app.

Unless you’re running integration tests, which is out of the scope of this article, now’s a good time to address unit tests with the help of patches. These are the cases where we’ll impersonate a result from a target making it look like the dependency is returning what you want it to; this makes it possible to test the actual application with that patched result.

Patching is the act of impersonating an external dependency such as a database, an API call on the internet, or even a library call, making it look like it has returned something we gave it to.

Talk is cheap, show me the code đŸ˜¶

If the above introduction doesn’t make any sense, hold your horses cause we’re about to get a couple of examples below to make sure everything clicks right where it should.

As you can see from above, I needed to mock an external library (requests to be specific). This enabled me to bypass the actual request from transmitting over the internet and as a result, I get to test only the application; which is very important to me because I would like to make sure that my application is working correctly if all the external libraries are behaving seamlessly.

It also empowers thou to measure thy coverage to see if there exists a dead code or not.

The monkeypatch, the argument to the test method, is a pytest built-in fixture that you can use in every single one of your test cases should you need it. It has quite a few very useful methods that we’re going to discuss in this article. One of the most useful ones being setattr; It makes it so that any access to a class instance or class method being redirected to something completely customizable.

In the above snippet, I am fake-replacing the requests.get to return a custom object (MockStatus). This custom object has an instance attribute that I need in my code status_code. Therefore when I call the requests.get(URL).status_code it’s actually not the actual library requests.Response.status_code, but MockStatus.status_code. Beautiful, isn’t it?đŸ˜»

Here’s the list of available methods from the documentation itself:

monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=False)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)

Note: setattr instance method has 2 signatures [source]:

  1. The first one is as we used above using a dot-separated modulename.classname.atttibutename as the first argument and a custom value in the second.
  2. The second one is passing a concrete object (remember, everything in python is an object) as the first argument, passing a double-quoted attribute name of that object (str) as the second argument, and a custom value as the final and third argument.

We’re gonna use both of these approaches in the next example; so stay calm, and “may the god of understanding guide us”. 🎼

Let’s get serious, do something practical😃

Now let’s get some other examples as well while we’re at it. And this time, let’s get a little bit serious and do something more useful. shall we?

I should warn you though that the next example is a little bit longer and you might get bored in the middle. But bear with me here as there are valuable lessons that took me a long time to realize.

As you can see, this is a fully functional application with my favorite framework.

But aside from all that endless battles of “my framework is better than yours”, let’s just take the lesson and move on.

I figure the source code is pretty obvious and doesn’t need me doing all the talking and making this article longer than it already is, decreasing my chance of having more readers 😁😉.

But just to have a reference, I’m gonna dive a little deeper into one of the test cases: test_create_new_user_successfully :

  • The client object is just a test application from the FastAPI itself and I didn’t do any magic except making it a globally accessible fixture to all my tests in this and any future python modules.
  • The monkeypatch, as mentioned earlier, is a fixture that needs no extra installation other than the pytest itself. Check the documentation for a complete API reference. So in each test case, you can see that I am mocking the database, making it “look-like” the database has returned something to the application, and testing if the source code could actually work with that mocked object.
  • At first, I am patching the first method of SQLAlchemy Query object; Query.first is equivalent to something similar to this: SELECT 
 FROM 
 LIMIT 1. By doing this patch we’re telling the interpreter to “assume that the database is giving you this”. This, in this scenario, is actually None, because I don’t want to get this error: Email already registered.
  • In the next part, I am iterating through every key-value pair of the randomly-generated-user, again telling the interpreter to fake-replace everything inside the ORM with the ones we’re giving, making it so that every access to any instance attribute would return the value we provided.
  • Session.commit and Session.refresh is also mocked, which might seem like an unnecessary thing to do as we have already got what we wanted in Query.first, but doing this will remove any actual database interaction that would otherwise need an up-and-running database.
  • The rest of the test case is just invoking the endpoint, fetching the result, and ensuring that what we received is exactly the same as what we expected it to be; which I’m sure you’re quite capable of figuring out but I didn’t want to be presumptuous nevertheless.
Young kids passionate about what they see
Photo by cottonbro from Pexels

Show me more, show me more, I can’t wait😋

Okay now. Do you feel like it’s been enough? Or should we go for another round? I feel like you get the point, but you know what they say?

The third time’s the charm.đŸ€™

Nothing too fancy here. Just trying to see if setenv and delenv are doing what they are supposed to. Just remember that doing delenv in the second test case isn’t actually necessary unless some evil-doer is running your test using the following command:

DATABASE_URI=i_am_not_empty pytest patch_environment.py

Which will fail if you do not have that delenv there. We also added raising=False which avoids empty values of that key from raising KeyError.

I know it’s been enough, but this is the final round. I promise đŸ€ž.

Alright. Enough is enough. I know that you’ve seen just enough to be sick of all the testing and everything. I was pretty excited about it when I first found out that there exists such a thing and this article is my appreciation to the outside world.

Kid running to school to learn new stuff
Photo by Ketut Subiyanto from Pexels

Time for goodbye đŸ„Č

I’m a huge fan of testing so it wouldn’t be inappropriate for me to do a little bit of self-promotion and tell you guys to check out my other related article as well đŸ˜đŸ‘»:

And that’s it y’all. Thanks very much for being supportive and reading till the end. Hope you enjoyed it and get something useful from it as well. And I hope you are as excited as I am (or even more) after finding out about this monkeypatch thing because I didn’t know about it until now.

Please leave your thoughts or any doubts you might have in the comments below. I’d be glad to have a chat regarding these fascinating tools. You can also mention any idea you might have regarding a new topic you want to learn, and I’ll gladly take the time to write an article for that as well. Don’t worry, I wouldn’t start unless I master it myself first 😅.

If you want to dig even deeper, here are the links you might need.

Automation tests

Mocks & Patches

Pytest

FastAPI

My Previous Contents

--

--