Reputation: 259
I'm trying to develop a test using pytest for a class method that randomly selects a string from a list of strings.
It looks essentially like the givemeanumber method below:
import os.path
from random import choice
class Bob(object):
def getssh():
return os.path.join(os.path.expanduser("~admin"), '.ssh')
def givemeanumber():
nos = [1, 2, 3, 4]
chosen = choice(nos)
return chosen
the first method, getssh, in the class Bob is just the example from the pytest docs
My production code fetches a list of strings from a DB and then randomly selects one. So I'd like my test to fetch the strings and then instead of randomly selecting, it selects the first string. That way I can test against a known string.
From my reading I reckon I need to use monkeypatching to fake the randomisation.
Here's what I've got so far
import os.path
from random import choice
from _pytest.monkeypatch import MonkeyPatch
from bob import Bob
class Testbob(object):
monkeypatch = MonkeyPatch()
def test_getssh(self):
def mockreturn(path):
return '/abc'
Testbob.monkeypatch.setattr(os.path, 'expanduser', mockreturn)
x = Bob.getssh()
assert x == '/abc/.ssh'
def test_givemeanumber(self):
Testbob.monkeypatch.setattr('random.choice', lambda x: x[0])
z = Bob.givemeanumber()
assert z == 1
The first test method is again the example from the pytest docs (adapted slightly as I'm using it in a test class). This works fine.
Following the example from the docs I would expect to use
Testbob.monkeypatch.setattr(random, 'choice', lambda x: x[0])
but this yields
NameError: name 'random' is not defined
if I change it to
Testbob.monkeypatch.setattr('random.choice', lambda x: x[0])
it gets further but no swapping out occurs:
AssertionError: assert 2 == 1
Is monkeypatching the right tool for the job? If it is where am I going wrong?
Upvotes: 4
Views: 11804
Reputation: 4085
I landed on this question because I wanted to get MonkeyPatch working.
To get it working, you don't write from _pytest.monkeypatch import MonkeyPatch
. The leading underscore is a hint this is an internal method.
If you look at the docs, MonkeyPatch is a fixture.
import pytest
def test_some_foobar_env_var(monkeypatch):
monkeypatch.setenv("SOME_ENV_VAR", "foobar")
assert something
More about fixtures
here.
Upvotes: 1
Reputation: 4189
The problem comes from how the variables names are handled in Python. The key difference from other languages is that there is NO assigments of the values to the variables by their name; there is only binding of the variables' names to the objects.
This is a bigger topic out of scope of this question, but the consequence is as follows:
When you import a function choice
from the module random
, you bind a name choice
to the function that exists there at the moment of import, and place this name in the local namespace of the bob
module.
When you patch the random.choice
, you re-bind the name choice
of module random
to the new mock object.
However, the already imported name in the bob
module still refers to the original function. Because nobody patched it. The function itself was NOT modified, just the name was replaced.
So, the Bob
class calls the original random.choice
function, not the mocked one.
To solve this problem, you can follow one of two ways (but not both, as they are conflicting):
A: Always call random.choice()
function by that exact full name (i.e. not choice
). And, of course, import random
before (not from random import ...
) — same as you do for os.path.expanduser()
.
# bob.py
import os.path
import random
class Bob(object):
@classmethod
def getssh(cls):
return os.path.join(os.path.expanduser("~admin"), '.ssh')
@classmethod
def givemeanumber(cls):
nos = [1, 2, 3, 4]
chosen = random.choice(nos) # <== !!! NOTE HERE !!!!
return chosen
B: Patch the actual function that you call, which is bob.choice()
in that case (not random.choice()
).
# test.py
import os.path
from _pytest.monkeypatch import MonkeyPatch
from bob import Bob
class Testbob(object):
monkeypatch = MonkeyPatch()
def test_givemeanumber(self):
Testbob.monkeypatch.setattr('bob.choice', lambda x: x[0])
z = Bob.givemeanumber()
assert z == 1
Regarding your original error with unknown name random
: If you watn to patch(random, 'choice', ...)
, then you have to import random
— i.e. bind the name random
to the module which is being patched.
When you do just from random import choice
, you bind the name choice
, but not random
to the local namespace of the variables.
Upvotes: 9