34

I'm trying to setup the target under test in @pytest.fixture and use it in all my tests in the module. I'm able to patch the test correctly, but after I add the @pytest.fixture to return the mock object and invoke the mocked object in other unit tests the object starting to refer back to the original function.

Following is the code I have. I was expecting the mocked_worker in the unit test to refer to the return value, but it is invoking the actual os.getcwd method instead.

Please help me correct the code:

import os
import pytest
from unittest.mock import patch

class Worker:
    def work_on(self):
        path = os.getcwd()
        print(f'Working on {path}')
        return path

@pytest.fixture()
def mocked_worker():
    with patch('test.test_module.os.getcwd', return_value="Testing"):
        result = Worker()
    return result

def test_work_on(mocked_worker):
    ans = mocked_worker.work_on()
    assert ans == "Testing"
Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
Bryan Cheng
  • 403
  • 1
  • 5
  • 11

3 Answers3

51

The problem is that when the worker returns the scope of "with" statement ends making the object take its real value, the solution is to use "yield".

@pytest.fixture()
def mocked_worker():
    with patch('test.test_module.os.getcwd', return_value="Testing"):
        result = Worker()
        yield result
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
27

I would recommend to use pytest-mock. So full example of one file (test_file.py) solution using this library would be:

import os
import pytest
from unittest.mock import patch

class Worker:
    def work_on(self):
        path = os.getcwd()
        print(f'Working on {path}')
        return path

@pytest.fixture()
def mocked_worker(mocker):  # mocker is pytest-mock fixture
    mocker.patch('test_file.os.getcwd', return_value="Testing")

def test_work_on(mocked_worker):
    worker = Worker()  # here we create instance of Worker, not mock itself!!
    ans = worker.work_on()
    assert ans == "Testing"

used libraries for reference:

pytest==5.3.0
pytest-mock==1.12.1

The Hog
  • 889
  • 10
  • 26
  • The only problem with pytest mock is that the mocker object is of scope "function" which can be pretty limiting in some situations. – Farhood ET Jan 12 '22 at 13:59
  • 2
    first thing - might be a good reason for that. Bigger scope might left thing mocked on the functions that you don't what to have it mocked (these are terrible bugs to deal with than you have a lot of tests) Second thing - you can just import the mokcer fuction, and create your own fixture with the scope you want: `import pytest;from pytest_mock.plugin import _mocker;your_mocker = pytest.fixture(scope="module")(_mocker)` p.s. [pytest source code](https://github.com/pytest-dev/pytest-mock/blob/main/src/pytest_mock/plugin.py) – The Hog Jan 13 '22 at 16:26
0

Another way to create a mock fixture without any additional package like pytest-mock is to use the following code convention:

import pytest
from unittest.mock import patch
from imagine import Worker

# in this example I want my fixture to be module scope since I want the 
# fixture will be called once during all module tests execution.

@pytest.fixture(scope="module")
@patch("test.test_module.os.getcwd")
def model_checker(mock_instance):
    mock_monitor.return_value = "Testing"
    worker_instance = Worker()
    return worker_instance

Please notice that the order which I put the decorators (@) is very important. You must first call the @patch and only after it you can put the rest of the decorators (in our case it's a pytest fixture). To understand more about the decorators order logic, I advise you to read more about Decorators & MagicMock in the python documentation.

Romano
  • 3
  • 1