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

Question:

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.

Asked By: Damon Stamper

||

Answers:

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!

Answered By: Damon Stamper

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
Answered By: wim
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.