What I Learned at Work this Week: Python patch and pytest fixture

Mike Diaz
5 min readJan 12, 2025

--

Photo by Pixabay: https://www.pexels.com/photo/ecg-machine-reads-134-263194/

My manager knows that I’m more comfortable working on Python than Java. So when I closed my last ticket of the sprint on a Thursday, he gave me a Python support ticket that I might be able to finish quickly, before the week ended. Working on this ticket revealed something important to me: I’m not necessarily better at Python, but I’m better at *my* Python. If I try to add a feature to somebody else’s code, it can be just as impenetrable as any other language.

I had plenty of resources to figure out what was going on. Besides the code itself, my manager gave me a rundown of the service (a lambda that reads and writes coupon codes) and shared documentation and even a draft PR where he attempted the fix. The documentation, which described how to surface errors in this service, led me to an example in the codebase that helped me implement the library. My problem came when I attempted to write a test for my code.

Running the test suite

Of course, I realize that I had already made a mistake by writing code first and a test second. Not that Test Driven Development is always a requirement, but I think it’s extra useful if you’re working in a new service because tests before features means you’re less likely to break existing functionality. With that in mind, I went to run the existing tests to make sure that my changes wouldn’t make any of them fail.

They all failed.

But it wasn’t because of my changes. There were issues with my local environment and the tests were attempting imports and looking for credentials that weren’t working. To debug, I’d have to understand what the tests were doing (again, something I should have tried before I started coding). It was at this point that I came across some unfamiliar concepts.

@patch.object

If you’re working on a new codebase, reading tests is a great place to start. Ideally, your service will have unit tests that illustrate its expected behavior. For example, the first test I saw looked like this:

@patch.object(FeatureFlagClient, "is_enabled")
@patch.object(CouponServiceClient, "get_coupon_pools_to_refresh")
def test_handler_single_coupon_enqueues_job(
get_coupon_pools_to_refresh, is_enabled, service, test_queue
):

Just reading the name, we know that something about this lambda handles a coupon and enqueues a job. Reading the logic of the test, I can understand a bit about how that works. But before I get there, I’d like to understand the annotations and the arguments in the definition. I’ve used @patch, but @patch.object was new to me.

Python documentation tells us that @patch.object will “patch the named member (attribute) on an object (target) with a mock object.” The target is the first argument passed to the decorator and the attribute is the second. It’s possible to pass a third argument, but we’re not seeing that here, so if you want to learn more, check out the doc.

Whether we’re using two or three arguments, the “target” is a class and the “attribute” is a method defined within that class. We use @patch.object to say “we’re going to mock that method and give it custom behavior.” We do this by passing it into the test as an argument, as we see with get_coupon_pools_to_refresh and is_enabled. Then, later on, we can tell it what to do, like this:

is_enabled.return_value = False

I might try to start using @patch.object more often —in some cases it’s easier than the classic @patch. At the very least, there aren’t issues with declaring the wrong path to the function/method, which always confuses me. But now I see what’s happening with the test definition and it looks like a pretty normal test. So why is it failing?

@pytest.fixture

One of my favorite strategies about debugging is to simplify until you have a working version of what you want, then re-add pieces until you find the issue. This works great with tests because ideally they’re broken into clear steps that you can peel away. All my tests were failing, which probably meant they all had the same issue. I commented out all but one and picked it apart, but still couldn’t see where the error was coming from. I started writing my own, simpler test, but it failed for the same reason. I wrote an extremely simple test that could not fail, but still got an error. I realized that I must be importing something problematic.

The tests directory contained two files: one with a bunch of tests and another called conftest.py. That file didn’t contain any test functions; instead it had a bunch of functions with the annotation @pytest.fixture. When I commented them all out, my errors were finally quieted. But I got different errors…because my tests wouldn’t run.

To describe a fixture, let’s go straight to the pytest documentation:

Software test fixtures initialize test functions. They provide a fixed baseline so that tests execute reliably and produce consistent, repeatable, results. Initialization may setup services, state, or other operating environments. These are accessed by test functions through arguments; for each fixture used by a test function there is typically a parameter (named after the fixture) in the test function’s definition.

We create a fixture when we want to replace something in our code that could be inconsistent or unreliable, like something that relies on a third party API or a database. This also helped answer a question I previously had about where the other arguments in my test definition came from:

def test_handler_single_coupon_enqueues_job(
get_coupon_pools_to_refresh, is_enabled, service, test_queue
):

Only the first two arguments were patched, but I realized that the last two were defined as fixtures in another file:

@pytest.fixture(scope="module")
def service(ssm):
lambda_module = __import__('path-to-lambda', fromlist=['file_name'])
service = lambda_module.service
service.InternalLibraryMySQLConnection = (
TestMySQLConnection
)
return service

This particular fixture takes the entire lambda we’re testing and replaces certain aspects that could cause inconsistent results, like a MySQL connection. I learned that one of the mocked functions created an SQS queue, but it appears that even the mock SQS queue interacts with AWS in a way that requires certain permissions that I hadn’t configured. Getting to the bottom of those credentials might take some time working with another team, but because I know exactly where the error is coming from, I can ask specific questions that will save everyone some grief.

Blind Spots

It feels a little silly to write this post. I finished boot camp close to five years ago and I still have to look up basic concepts about testing my code. After studying for five years, my peers might have Master’s degrees or be Senior Engineers. They might have started their own company and made a million dollars. I’m not close to doing any of those things.

I consider myself fortunate because I don’t want to do those things and I don’t have to do those things. I’m lucky to be in an industry where I don’t have to push to be the best just to survive. I can learn and grow at my own pace and if I “fall behind” or make a mistake that I “shouldn’t” make, that’s okay. Maybe someday my luck will run out and I will have to learn faster. Until then, it’s a good situation!

Sources

--

--

Mike Diaz
Mike Diaz

No responses yet