Pytest, how to use a context manager to allow Mock to be used only for certain code?

Viewed 29

I'm trying to use Pytest to test Python code. The code I'm testing isn't a class but is a bunch of functions in the same file that call other functions in the same file. For example advanced_function1 calls basic_function, and advanced_function2 can also call basic_function.

When I file.basic_function = Mock() in a test it isn't scoped to just that test.

I can verify this because when I run the test (test_advanced_function1) that mocks basic_function then test_advanced_function2 does not pass. However if I only run test_advanced_function2 (meaning basic_function never gets mocked in another test) then it works.

To my knowledge the way to correct this is to use a context manager in test_advanced_function1 and mock basic_function as that context manager. I don't know how to do this.

To simplify things and more directly declare intent I've got a test that checks if the function is a Mock object or not.

myfile.py

def basic_function():
    return True

def advanced_function1():
    return basic_function()

def advanced_function2():
    return not basic_function()

test_myfile.py

from unittest.mock import Mock

import myfile

def test_basic_function(monkeypatch):
    assert myfile.basic_function() == True
    
def test_advanced_function1():
    myfile.basic_function = Mock(return_value='foo')
    assert myfile.basic_function() == 'foo'
    
def test_advanced_function2():
    assert not isinstance(myfile.basic_function, Mock)

So, how do I use a context manager in test_advanced_function1 to mock basic_function?

EDIT: To clarify, I can't assign the mock as a fixture because there are multiple test cases that run in the real code I'm working on and I can't mock basic_function for all the asserts in advanced_function1. Yes I know I should break this massive test apart into smaller tests but I'm just learning the codebase and don't want to change too much before getting all their tests working again.

2 Answers

The mock.patch context manager is seldom used directly in pytest. Instead we use fixtures. The plugin pytest-mock provides a fixture for using the mock.patch API in a more "pytest-thonic" way, like this:

import pytest

import myfile

def test_basic_function():
    assert myfile.basic_function() == True
    
def test_advanced_function1(mocker):
    mocker.patch("myfile.basic_function", return_value="foo")
    assert myfile.advanced_function1() == 'foo'
    
def test_advanced_function2():
    assert myfile.advanced_function2() == False

One possible solution is to use patch in a context manager. test_myfile.py

from unittest.mock import patch
from unittest.mock import Mock

import myfile

def test_basic_function(monkeypatch):
    assert myfile.basic_function() == True
    
def test_advanced_function1():
    with patch('myfile.basic_function') as basic_function:
        basic_function.return_value = 'foo'
        assert myfile.basic_function() == 'foo'
    
def test_advanced_function2():
    assert not isinstance(myfile.basic_function, Mock)

But please let me know if there's a more elegant way to do this!

Related