Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

pytest - monkeypatch keyword argument default

Tags:

I'd like to test the default behavior of a function. I have the following:

# app/foo.py
DEFAULT_VALUE = 'hello'

def bar(text=DEFAULT_VALUE):
    print(text)
# test/test_app.py
import app

def test_app(monkeypatch):
    monkeypatch.setattr('app.foo.DEFAULT_VALUE', 'patched')
    app.foo.bar()
    assert 0

Output is hello; not what I wanted.

One solution is to pass the default value explicitly: app.foo.bar(text=app.foo.DEFAULT_VALUE).

But I find it interesting that this doesn't seem to be an issue when defaulting to the global scope:

# app/foo.py
DEFAULT_VALUE = 'hello'

def bar():
    print(DEFAULT_VALUE)

Output is patched.

Why does this happen? And is there a better solution than passing the default explicitly?

like image 891
Nathaniel Jones Avatar asked Dec 17 '18 19:12

Nathaniel Jones


1 Answers

Function defaults are bound at function definition time.

By the time you are in test code, the module in which the function was defined has already been imported and it is too late to swap out the default by monkeypatching on the module level constant. That name was already resolved.

A workaround is to define the function like this:

def bar(text=None):
    if text is None:
        text = DEFAULT_VALUE
    print(text)

Now the default value is looked up at function call time, which means a monkeypatch on the module level default will still work.

If you don't like to modify the function definition, then you can monkeypatch the function object itself:

monkeypatch.setattr("app.foo.bar.__defaults__", ("test_hello",))
like image 98
wim Avatar answered Nov 15 '22 06:11

wim