2

I have a question related the answer of this question:

pytest: setup a mock for every test function

I like the idea of using functions that receive the mock objects via arguments. In this way, the setting up of mocks can be reused. I conclude from the answer that mock objects are mutable in Python and changing them inside the function will have the side effect that they are changed outside. However, I consider it as dangerous to have side effects. So, I suggest the followoing:

def test(self, mock1):
    mock1 = setup_mock1_to_always_xxx(mock1)
    ...

with

def setup_mock1_to_always_xxx(mock1):
    # create a copy to avoid side effects
    mock1 = mock1.copy() # how to copy a Mock?
    mock1.return_value = "my return value"
    return mock1

Would this be possible?

hoefling
  • 59,418
  • 12
  • 147
  • 194
  • 2
    Isn't it easier to just setup another mock rather then copying? `mock1` in your example can be a fixture with a function scope that will be called before the execution of each test, so each test gets a fresh mock supplied. – hoefling Jan 23 '20 at 00:26
  • @hoefling, I think this is the most elegant answer. Would you like to make an answer out of your comment, maybe with giving an example fixture? I don't want to take your credits, otherwise I will do it. – Vanessa Klaas Jan 23 '20 at 04:52

1 Answers1

3

I would suggest injecting mocks using pytest fixtures instead of manual mock copying. The function-scoped fixtures (the default ones) reevaluate for each test. Example: assume you have a module

# mod.py

def spam():
    return eggs()


def eggs():
    return "eggs"

and a test

from unittest.mock import patch
from mod import spam


@patch("mod.eggs")
def test_bacon(mock1):
    mock1.return_value = "bacon"
    assert spam() == "bacon"

and now you want to refactor it into testing against bacon and bacon with eggs. Move out the patching inside a fixture:

import pytest
from unittest.mock import patch
from mod import spam


@pytest.fixture
def eggs_mock():
    with patch("mod.eggs") as mock1:
        yield mock1


def test_bacon(eggs_mock):
    eggs_mock.return_value = "bacon"
    assert spam() == "bacon"


def test_bacon_with_eggs(eggs_mock):
    eggs_mock.return_value = "bacon with eggs"
    assert spam() == "bacon with eggs"

You now have two different mocks of the mod.eggs function, one unique mock in each test.

unittest-style tests

This approach also works with unittest test classes, although the injection is a bit more verbose since unittest.TestCases don't accept arguments in test methods. This is the same approach as described in this answer of mine. In the example below, I store the eggs_mock fixture return value in a Tests instance attribute via using an additional autouse fixture:

from unittest import TestCase
from unittest.mock import patch
import pytest
from mod import spam


@pytest.fixture
def eggs_mock():
    with patch("mod.eggs") as mock1:
        yield mock1


class Tests(TestCase):

    @pytest.fixture(autouse=True)
    def inject_eggs_mock(self, eggs_mock):
        self._eggs_mock = eggs_mock

    def test_bacon(self):
        self._eggs_mock.return_value = "bacon"
        assert spam() == "bacon"


    def test_bacon_with_eggs(self):
        self._eggs_mock.return_value = "bacon with eggs"
        assert spam() == "bacon with eggs"
hoefling
  • 59,418
  • 12
  • 147
  • 194