pytest-tricks icon indicating copy to clipboard operation
pytest-tricks copied to clipboard

Use @patch and @pytest.mark.parametrize at the same time

Open tomschr opened this issue 7 years ago • 13 comments

Hi,

great project! 👍 Thanks for all the efforts!

I would suggest to add another tip to your great site: how to correctly use patch and pytest.mark.parametrize at the same time. Here is a small, stupid test snippet:

import os.path
from unittest.mock import patch

def exists(path):
    return os.path.exists(path)

@patch('os.path.exists')
@pytest.mark.parametrize('path, expect', [('/foo', True), ('/bar', False)])
def test_exists(mock_exists, path, expect):
    def side_effect(args):
        return expect
    mock_exists.side_effect = side_effect
    assert exists(path) == expect

This order would also work:

@pytest.mark.parametrize('path, expect', [('/foo', True), ('/bar', False)])
@patch('os.path.exists')
def test_exists(mock_exists, path, expect):

However, other orders or combinations won't.

Could be useful for others too, because I always have to try it out and never get it right in the first place. 😆

tomschr avatar May 07 '17 18:05 tomschr

Hi @tomschr! 👋

Thank you for the kind words and your suggestion. Much appreciated! 😊

I use pytest-mock in my projects, when I need to mock things. Have you tried that before?

# -*- coding: utf-8 -*-

import os

import pytest


def exists(path):
    return os.path.exists(path)


@pytest.fixture(autouse=True)
def mock_exists(mocker, expect):
    mocker.patch('os.path.exists', return_value=expect)


@pytest.mark.parametrize(
    "path, expect",
    [('foo', True), ('bar', False)],
)
def test_exists(path, expect):
    assert exists(path) == expect

/cc @nicoddemus ⬆️

hackebrot avatar May 07 '17 20:05 hackebrot

I use pytest-mock in my projects, when I need to mock things. Have you tried that before?

I've heard about it before and I think, I've tried it also in the past - although I hardly remember. 😄
Thanks for the snippet, very much appreciated!

However, this introduce another dependency which I would like to avoid. The unittest.mock module is available in the standard library. Another library (although probably very great) introduce another "moving target".

Maybe your suggestion would be something for another great article! 😊 One which use the standard module and another one with pytest-mock. 😊

tomschr avatar May 07 '17 21:05 tomschr

@hackebrot Any idea how I can do this only for one test? autouse=True makes this global

noorul avatar Jun 27 '18 03:06 noorul

The suggestion above for using a fixture works if you're injecting a dependency through constructor or method call.

If you're wanting to patch something low level like for example yourlib.api.request (requests dependency), then it makes a little more sense to me to include

@pytest.mark.integration
@pytest.mark.parametrize(
    ('param1', 'param2',),
    [
    ]
)
@mock.patch('yourlib.api.request')
def test_something(
    [self],
    mock_response,
    fixture1, fixture2, fixture3,
    param1, param2,
):
    mock_response.return_value = MockJsonResponse([], 200)
    response = yourlib.api.do_something_using_request()
    assert response.status_code == 200
    assert len(response.json()) == 0

Warning: this is pseudo-code

Lewiscowles1986 avatar Nov 23 '18 11:11 Lewiscowles1986

@noorul use mark.usefixtures marker. Above example, revisited:


@pytest.fixture
def mock_exists(mocker, expect):
    mocker.patch('os.path.exists', return_value=expect)


@pytest.mark.usefixtures('mock_exists')
@pytest.mark.parametrize(
    "path, expect",
    [('foo', True), ('bar', False)],
)
def test_exists(path, expect):
    assert exists(path) == expect


def test_without_mocking():
    assert os.path.exists != exists

hoefling avatar Feb 07 '19 11:02 hoefling

@hoefling What is the standalone exists in the above snippet?

haridsv avatar Aug 17 '22 06:08 haridsv

os.path.exists

nicoddemus avatar Aug 17 '22 11:08 nicoddemus

@nicoddemus I am not sure, I added the below imports at the beginning:

import os.path
from os.path import exists
import pytest

and got these failures:

___________________________________________________________________________________________________________________ test_exists[foo-True] ____________________________________________________________________________________________________________________

path = 'foo', expect = True

    @pytest.mark.usefixtures('mock_exists')
    @pytest.mark.parametrize(
        "path, expect",
        [('foo', True), ('bar', False)],
    )
    def test_exists(path, expect):
>       assert exists(path) == expect
E       AssertionError: assert False == True
E        +  where False = exists('foo')

/tmp/t.py:16: AssertionError
____________________________________________________________________________________________________________________ test_without_mocking ____________________________________________________________________________________________________________________

    def test_without_mocking():
>       assert os.path.exists != exists
E       AssertionError: assert <function exists at 0x10a5a7b80> != exists
E        +  where <function exists at 0x10a5a7b80> = <module 'posixpath' from '/usr/local/Cellar/[email protected]/3.9.1_3/Frameworks/Python.framework/Versions/3.9/lib/python3.9/posixpath.py'>.exists
E        +    where <module 'posixpath' from '/usr/local/Cellar/[email protected]/3.9.1_3/Frameworks/Python.framework/Versions/3.9/lib/python3.9/posixpath.py'> = os.path

/tmp/t.py:20: AssertionError

haridsv avatar Aug 17 '22 13:08 haridsv

It is, but the example is incomplete, I think it should be:

import pytest
import os.path


@pytest.fixture
def mock_exists(mocker, expect):
    mocker.patch('os.path.exists', return_value=expect)


@pytest.mark.usefixtures('mock_exists')
@pytest.mark.parametrize(
    "path, expect",
    [('foo', True), ('bar', False)],
)
def test_exists(path, expect):
    assert os.path.exists(path) == expect

nicoddemus avatar Aug 17 '22 13:08 nicoddemus

@nicoddemus Yes, that is exactly what I did for that test to get it to pass, but I couldn't make out what change goes into test_without_mocking.

haridsv avatar Aug 17 '22 14:08 haridsv

Not sure, we will need to ask @hoefling

nicoddemus avatar Aug 17 '22 14:08 nicoddemus

It's also possible to use patch as a context manager:

with patch('os.path.exists'):
    ... code ...

chadfurman avatar Oct 18 '22 13:10 chadfurman

@haridsv a bit late to the party, but the test_without_mocking is just there to demonstrate that no mocking is done when the fixture is not requested via usefixtures marker. exists is defined by the OP via

def exists(path):
    return os.path.exists(path)

in the original post.

hoefling avatar Oct 18 '22 18:10 hoefling