import os import platform import sys from contextlib import contextmanager import py.code import pytest pytest_plugins = "pytester" # could not make some of the tests work on PyPy, patches are welcome! skip_pypy = pytest.mark.skipif( platform.python_implementation() == "PyPy", reason="could not make work on pypy" ) # Python 3.8 changed the output formatting (bpo-35500). PY38 = sys.version_info >= (3, 8) @pytest.fixture def needs_assert_rewrite(pytestconfig): """ Fixture which skips requesting test if assertion rewrite is disabled (#102) Making this a fixture to avoid acessing pytest's config in the global context. """ option = pytestconfig.getoption("assertmode") if option != "rewrite": pytest.skip( "this test needs assertion rewrite to work but current option " 'is "{}"'.format(option) ) class UnixFS(object): """ Wrapper to os functions to simulate a Unix file system, used for testing the mock fixture. """ @classmethod def rm(cls, filename): os.remove(filename) @classmethod def ls(cls, path): return os.listdir(path) @pytest.fixture def check_unix_fs_mocked(tmpdir, mocker): """ performs a standard test in a UnixFS, assuming that both `os.remove` and `os.listdir` have been mocked previously. """ def check(mocked_rm, mocked_ls): assert mocked_rm is os.remove assert mocked_ls is os.listdir file_name = tmpdir / "foo.txt" file_name.ensure() UnixFS.rm(str(file_name)) mocked_rm.assert_called_once_with(str(file_name)) assert os.path.isfile(str(file_name)) mocked_ls.return_value = ["bar.txt"] assert UnixFS.ls(str(tmpdir)) == ["bar.txt"] mocked_ls.assert_called_once_with(str(tmpdir)) mocker.stopall() assert UnixFS.ls(str(tmpdir)) == ["foo.txt"] UnixFS.rm(str(file_name)) assert not os.path.isfile(str(file_name)) return check def mock_using_patch_object(mocker): return mocker.patch.object(os, "remove"), mocker.patch.object(os, "listdir") def mock_using_patch(mocker): return mocker.patch("os.remove"), mocker.patch("os.listdir") def mock_using_patch_multiple(mocker): r = mocker.patch.multiple("os", remove=mocker.DEFAULT, listdir=mocker.DEFAULT) return r["remove"], r["listdir"] @pytest.mark.parametrize( "mock_fs", [mock_using_patch_object, mock_using_patch, mock_using_patch_multiple] ) def test_mock_patches(mock_fs, mocker, check_unix_fs_mocked): """ Installs mocks into `os` functions and performs a standard testing of mock functionality. We parametrize different mock methods to ensure all (intended, at least) mock API is covered. """ # mock it twice on purpose to ensure we unmock it correctly later mock_fs(mocker) mocked_rm, mocked_ls = mock_fs(mocker) check_unix_fs_mocked(mocked_rm, mocked_ls) mocker.resetall() mocker.stopall() def test_mock_patch_dict(mocker): """ Testing :param mock: """ x = {"original": 1} mocker.patch.dict(x, values=[("new", 10)], clear=True) assert x == {"new": 10} mocker.stopall() assert x == {"original": 1} def test_mock_patch_dict_resetall(mocker): """ We can call resetall after patching a dict. :param mock: """ x = {"original": 1} mocker.patch.dict(x, values=[("new", 10)], clear=True) assert x == {"new": 10} mocker.resetall() assert x == {"new": 10} def test_deprecated_mock(testdir): """ Use backward-compatibility-only mock fixture to ensure complete coverage. """ p1 = testdir.makepyfile( """ import os def test(mock, tmpdir): mock.patch("os.listdir", return_value=["mocked"]) assert os.listdir(str(tmpdir)) == ["mocked"] mock.stopall() assert os.listdir(str(tmpdir)) == [] """ ) result = testdir.runpytest(str(p1)) result.stdout.fnmatch_lines( ['*DeprecationWarning: "mock" fixture has been deprecated, use "mocker"*'] ) assert result.ret == 0 @pytest.mark.parametrize( "name", [ "ANY", "call", "create_autospec", "MagicMock", "Mock", "mock_open", "NonCallableMock", "PropertyMock", "sentinel", ], ) def test_mocker_aliases(name, pytestconfig): from pytest_mock import _get_mock_module, MockFixture mock_module = _get_mock_module(pytestconfig) mocker = MockFixture(pytestconfig) assert getattr(mocker, name) is getattr(mock_module, name) def test_mocker_resetall(mocker): listdir = mocker.patch("os.listdir") open = mocker.patch("os.open") listdir("/tmp") open("/tmp/foo.txt") listdir.assert_called_once_with("/tmp") open.assert_called_once_with("/tmp/foo.txt") mocker.resetall() assert not listdir.called assert not open.called class TestMockerStub: def test_call(self, mocker): stub = mocker.stub() stub("foo", "bar") stub.assert_called_once_with("foo", "bar") def test_repr_with_no_name(self, mocker): stub = mocker.stub() assert "name" not in repr(stub) def test_repr_with_name(self, mocker): test_name = "funny walk" stub = mocker.stub(name=test_name) assert "name={0!r}".format(test_name) in repr(stub) def __test_failure_message(self, mocker, **kwargs): expected_name = kwargs.get("name") or "mock" if PY38: msg = "expected call not found.\nExpected: {0}()\nActual: not called." else: msg = "Expected call: {0}()\nNot called" expected_message = msg.format(expected_name) stub = mocker.stub(**kwargs) with pytest.raises(AssertionError) as exc_info: stub.assert_called_with() assert str(exc_info.value) == expected_message def test_failure_message_with_no_name(self, mocker): self.__test_failure_message(mocker) @pytest.mark.parametrize("name", (None, "", "f", "The Castle of aaarrrrggh")) def test_failure_message_with_name(self, mocker, name): self.__test_failure_message(mocker, name=name) def test_instance_method_spy(mocker): class Foo(object): def bar(self, arg): return arg * 2 foo = Foo() other = Foo() spy = mocker.spy(foo, "bar") assert foo.bar(arg=10) == 20 assert other.bar(arg=10) == 20 foo.bar.assert_called_once_with(arg=10) spy.assert_called_once_with(arg=10) @skip_pypy def test_instance_method_by_class_spy(mocker): class Foo(object): def bar(self, arg): return arg * 2 spy = mocker.spy(Foo, "bar") foo = Foo() other = Foo() assert foo.bar(arg=10) == 20 assert other.bar(arg=10) == 20 calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)] assert spy.call_args_list == calls @skip_pypy def test_instance_method_by_subclass_spy(mocker): class Base(object): def bar(self, arg): return arg * 2 class Foo(Base): pass spy = mocker.spy(Foo, "bar") foo = Foo() other = Foo() assert foo.bar(arg=10) == 20 assert other.bar(arg=10) == 20 calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)] assert spy.call_args_list == calls @skip_pypy def test_class_method_spy(mocker): class Foo(object): @classmethod def bar(cls, arg): return arg * 2 spy = mocker.spy(Foo, "bar") assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) spy.assert_called_once_with(arg=10) @skip_pypy @pytest.mark.xfail(sys.version_info[0] == 2, reason="does not work on Python 2") def test_class_method_subclass_spy(mocker): class Base(object): @classmethod def bar(self, arg): return arg * 2 class Foo(Base): pass spy = mocker.spy(Foo, "bar") assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) spy.assert_called_once_with(arg=10) @skip_pypy def test_class_method_with_metaclass_spy(mocker): class MetaFoo(type): pass class Foo(object): __metaclass__ = MetaFoo @classmethod def bar(cls, arg): return arg * 2 spy = mocker.spy(Foo, "bar") assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) spy.assert_called_once_with(arg=10) @skip_pypy def test_static_method_spy(mocker): class Foo(object): @staticmethod def bar(arg): return arg * 2 spy = mocker.spy(Foo, "bar") assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) spy.assert_called_once_with(arg=10) @skip_pypy @pytest.mark.xfail(sys.version_info[0] == 2, reason="does not work on Python 2") def test_static_method_subclass_spy(mocker): class Base(object): @staticmethod def bar(arg): return arg * 2 class Foo(Base): pass spy = mocker.spy(Foo, "bar") assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) spy.assert_called_once_with(arg=10) @contextmanager def assert_traceback(): """ Assert that this file is at the top of the filtered traceback """ try: yield except AssertionError: traceback = py.code.ExceptionInfo().traceback crashentry = traceback.getcrashentry() assert crashentry.path == __file__ else: raise AssertionError("DID NOT RAISE") @contextmanager def assert_argument_introspection(left, right): """ Assert detailed argument introspection is used """ try: yield except AssertionError as e: # this may be a bit too assuming, but seems nicer then hard-coding import _pytest.assertion.util as util # NOTE: we assert with either verbose or not, depending on how our own # test was run by examining sys.argv verbose = any(a.startswith("-v") for a in sys.argv) expected = "\n ".join(util._compare_eq_iterable(left, right, verbose)) assert expected in str(e) else: raise AssertionError("DID NOT RAISE") @pytest.mark.skipif( sys.version_info[:2] == (3, 4), reason="assert_not_called not available in Python 3.4", ) def test_assert_not_called_wrapper(mocker): stub = mocker.stub() stub.assert_not_called() stub() with assert_traceback(): stub.assert_not_called() def test_assert_called_with_wrapper(mocker): stub = mocker.stub() stub("foo") stub.assert_called_with("foo") with assert_traceback(): stub.assert_called_with("bar") def test_assert_called_once_with_wrapper(mocker): stub = mocker.stub() stub("foo") stub.assert_called_once_with("foo") stub("foo") with assert_traceback(): stub.assert_called_once_with("foo") def test_assert_called_once_wrapper(mocker): stub = mocker.stub() if not hasattr(stub, "assert_called_once"): pytest.skip("assert_called_once not available") stub("foo") stub.assert_called_once() stub("foo") with assert_traceback(): stub.assert_called_once() def test_assert_called_wrapper(mocker): stub = mocker.stub() if not hasattr(stub, "assert_called"): pytest.skip("assert_called_once not available") with assert_traceback(): stub.assert_called() stub("foo") stub.assert_called() stub("foo") stub.assert_called() @pytest.mark.usefixtures("needs_assert_rewrite") def test_assert_called_args_with_introspection(mocker): stub = mocker.stub() complex_args = ("a", 1, set(["test"])) wrong_args = ("b", 2, set(["jest"])) stub(*complex_args) stub.assert_called_with(*complex_args) stub.assert_called_once_with(*complex_args) with assert_argument_introspection(complex_args, wrong_args): stub.assert_called_with(*wrong_args) stub.assert_called_once_with(*wrong_args) @pytest.mark.usefixtures("needs_assert_rewrite") def test_assert_called_kwargs_with_introspection(mocker): stub = mocker.stub() complex_kwargs = dict(foo={"bar": 1, "baz": "spam"}) wrong_kwargs = dict(foo={"goo": 1, "baz": "bran"}) stub(**complex_kwargs) stub.assert_called_with(**complex_kwargs) stub.assert_called_once_with(**complex_kwargs) with assert_argument_introspection(complex_kwargs, wrong_kwargs): stub.assert_called_with(**wrong_kwargs) stub.assert_called_once_with(**wrong_kwargs) def test_assert_any_call_wrapper(mocker): stub = mocker.stub() stub("foo") stub("foo") stub.assert_any_call("foo") with assert_traceback(): stub.assert_any_call("bar") def test_assert_has_calls(mocker): stub = mocker.stub() stub("foo") stub.assert_has_calls([mocker.call("foo")]) with assert_traceback(): stub.assert_has_calls([mocker.call("bar")]) def test_monkeypatch_ini(mocker, testdir): # Make sure the following function actually tests something stub = mocker.stub() assert stub.assert_called_with.__module__ != stub.__module__ testdir.makepyfile( """ import py.code def test_foo(mocker): stub = mocker.stub() assert stub.assert_called_with.__module__ == stub.__module__ """ ) testdir.makeini( """ [pytest] mock_traceback_monkeypatch = false """ ) result = runpytest_subprocess(testdir) assert result.ret == 0 def test_parse_ini_boolean(): import pytest_mock assert pytest_mock.parse_ini_boolean("True") is True assert pytest_mock.parse_ini_boolean("false") is False with pytest.raises(ValueError): pytest_mock.parse_ini_boolean("foo") def test_patched_method_parameter_name(mocker): """Test that our internal code uses uncommon names when wrapping other "mock" methods to avoid conflicts with user code (#31). """ class Request: @classmethod def request(cls, method, args): pass m = mocker.patch.object(Request, "request") Request.request(method="get", args={"type": "application/json"}) m.assert_called_once_with(method="get", args={"type": "application/json"}) def test_monkeypatch_native(testdir): """Automatically disable monkeypatching when --tb=native. """ testdir.makepyfile( """ def test_foo(mocker): stub = mocker.stub() stub(1, greet='hello') stub.assert_called_once_with(1, greet='hey') """ ) result = runpytest_subprocess(testdir, "--tb=native") assert result.ret == 1 assert "During handling of the above exception" not in result.stdout.str() assert "Differing items:" not in result.stdout.str() traceback_lines = [ x for x in result.stdout.str().splitlines() if "Traceback (most recent call last)" in x ] assert ( len(traceback_lines) == 1 ) # make sure there are no duplicated tracebacks (#44) def test_monkeypatch_no_terminal(testdir): """Don't crash without 'terminal' plugin. """ testdir.makepyfile( """ def test_foo(mocker): stub = mocker.stub() stub(1, greet='hello') stub.assert_called_once_with(1, greet='hey') """ ) result = runpytest_subprocess(testdir, "-p", "no:terminal", "-s") assert result.ret == 1 assert result.stdout.lines == [] @pytest.mark.skipif(sys.version_info[0] < 3, reason="Py3 only") def test_standalone_mock(testdir): """Check that the "mock_use_standalone" is being used. """ testdir.makepyfile( """ def test_foo(mocker): pass """ ) testdir.makeini( """ [pytest] mock_use_standalone_module = true """ ) result = runpytest_subprocess(testdir) assert result.ret == 3 result.stderr.fnmatch_lines(["*No module named 'mock'*"]) def runpytest_subprocess(testdir, *args): """Testdir.runpytest_subprocess only available in pytest-2.8+""" if hasattr(testdir, "runpytest_subprocess"): return testdir.runpytest_subprocess(*args) else: # pytest 2.7.X return testdir.runpytest(*args) @pytest.mark.usefixtures("needs_assert_rewrite") def test_detailed_introspection(testdir): """Check that the "mock_use_standalone" is being used. """ testdir.makepyfile( """ def test(mocker): m = mocker.Mock() m('fo') m.assert_called_once_with('', bar=4) """ ) result = testdir.runpytest("-s") if PY38: expected_lines = [ "*AssertionError: expected call not found.", "*Expected: mock('', bar=4)", "*Actual: mock('fo')", ] else: expected_lines = [ "*AssertionError: Expected call: mock('', bar=4)*", "*Actual call: mock('fo')*", ] expected_lines += [ "*pytest introspection follows:*", "*Args:", "*assert ('fo',) == ('',)", "*At index 0 diff: 'fo' != ''*", "*Use -v to get the full diff*", "*Kwargs:*", "*assert {} == {'bar': 4}*", "*Right contains more items:*", "*{'bar': 4}*", "*Use -v to get the full diff*", ] result.stdout.fnmatch_lines(expected_lines) def test_assert_called_with_unicode_arguments(mocker): """Test bug in assert_call_with called with non-ascii unicode string (#91)""" stub = mocker.stub() stub(b"l\xc3\xb6k".decode("UTF-8")) with pytest.raises(AssertionError): stub.assert_called_with(u"lak") def test_plain_stopall(testdir): """patch.stopall() in a test should not cause an error during unconfigure (#137)""" testdir.makepyfile( """ import random def get_random_number(): return random.randint(0, 100) def test_get_random_number(mocker): patcher = mocker.mock_module.patch("random.randint", lambda x, y: 5) patcher.start() assert get_random_number() == 5 mocker.mock_module.patch.stopall() """ ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines("* 1 passed in *") assert "RuntimeError" not in result.stderr.str()