Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting monkeypatching in pytest to work

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?

like image 501
Pierre Brasseau Avatar asked Oct 19 '25 16:10

Pierre Brasseau


1 Answers

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:

  1. 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.

  2. When you patch the random.choice, you re-bind the name choice of module random to the new mock object.

  3. 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.

  4. 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.

like image 116
Sergey Vasilyev Avatar answered Oct 21 '25 05:10

Sergey Vasilyev



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!