fgblomqvist
fgblomqvist

Reputation: 2424

Mocking a function that is passed in as a parameter during a class variable initialization

scuevals_api/resources/students.py:

def year_in_range(year):
    return datetime.now().year <= year <= datetime.now().year + 10


class StudentsResource(Resource):
    args = {
        'graduation_year': fields.Int(required=True, validate=year_in_range),
    }

    ...

I'm trying to mock year_in_range (to always return True) however, all my attempts have failed so far.

I'm using the decorator approach with mock.patch and have tried a ton of different targets, but the one I believe should be the correct one is: @mock.patch('scuevals_api.resources.students.year_in_range', return_value=True)

The mock function never gets called, as in, it's not mocking correctly. I'm not getting any errors either.

My only remaining suspicions is that it has something to do with that the function is passed in to fields.Int as a param (hence the question title), but in my head, it shouldn't affect anything.

I'm clueless as to where this function should be mocked?

Upvotes: 2

Views: 140

Answers (2)

fgblomqvist
fgblomqvist

Reputation: 2424

Thanks to the explanation by Chris Hunt, I came up with an alternative solution. It does modify the application code rather than the testing code, but if that is acceptable (which, in today's day and age probably should be, since having testable code is high priority), it is a really simple solution:

It's not possible to mock year_in_range since a reference to the original function is saved before the mocking is done. Therefore, "wrap" the function you want to mock with another function and pass the wrapper instead. Wrapping can be done in a nice and tidy way using lambda functions:

def year_in_range(year):
    return datetime.now().year <= year <= datetime.now().year + 10


class StudentsResource(Resource):
    args = {
        'graduation_year': fields.Int(required=True, validate=lambda y: year_in_range(y)),
    }

    ...

Now, when I mock year_in_range as stated in the question, it will work. The reason is because now a reference is saved to the lambda function, instead of to the original year_in_range (that won't be accessed until the lambda function runs, which will be during the test).

Upvotes: 1

Chris Hunt
Chris Hunt

Reputation: 4030

By the time mock has patched year_in_range it is too late. mock.patch imports the module specified by the string you provided and patches the name indicated within the module so it refers to a mock object - it does not fundamentally alter the function object itself. On import of scuevals_api.resources.students the body of the StudentsResource class will be executed and a reference to the original year_in_range saved within the StudentResource.args['graduation_year'] object, as a result making the name year_in_range refer to a mock object has no impact.

In this particular case you have a few options:

  1. Assuming you're trying to test some functionality, instead of trying to mock year_in_range you can seed the database (?) with data that tests the condition
  2. You can patch datetime.now which will be called by year_in_range
  3. You can patch the member of StudentResource.args['graduation_year'] where the function passed to validate has been saved.

Upvotes: 2

Related Questions