Python3 + pytest + pytest-mock: Mocks просачивается в другие тестовые функции, нарушая утверждения?

ПРИМЕЧАНИЕ. Все сведения о моей настройке (версия python, модули и т.д.) указаны в нижней части вопроса.

Извините заранее, если эта проблема вопиющая, но я боролся с ней уже несколько дней. Надеюсь, кто-то может пролить некоторый новый свет.

Я собираюсь преобразовать модульные тесты для моего личного проекта из unittestpytest. Раньше я использовал встроенный модуль unittest.mock, но теперь я пытаюсь использовать плагин pytest-mock.

У меня есть крошечное чувство, что мои тесты просачивают макет объектов друг в друга.

Здесь почему:

Детали высокого уровня:

# Python version
Python 3.5.2

# Pytest version ( and plugins )
pytest==3.0.7
pytest-benchmark==3.1.0a2
pytest-catchlog==1.2.2
pytest-cov==2.4.0
pytest-ipdb==0.1.dev2
pytest-leaks==0.2.2
pytest-mock==1.6.0
pytest-rerunfailures==2.1.0
pytest-sugar==0.8.0
pytest-timeout==1.2.0
python-dateutil==2.6.0
python-dbusmock==0.16.7

Когда я запускаю свои тесты, используя следующую команду:

py.test --pdb --showlocals -v -R : -k test_subprocess.py

Все в порядке, пока мы не дойдем до test_subprocess_check_command_type. В этот момент я получаю следующую ошибку:

        # Set mock return types
        # mock_map_type_to_command.return_value = int

        # action
        with pytest.raises(TypeError) as excinfo:
            scarlett_os.subprocess.Subprocess(test_command,
                                              name=test_name,
                                              fork=test_fork,
>                                             run_check_command=True)
E           Failed: DID NOT RAISE <class 'TypeError'>

excinfo    = <[AttributeError("'ExceptionInfo' object has no attribute 'typename'") raised in repr()] ExceptionInfo object at 0x7f8c380f9dc0>
mock_fork  = <Mock name='mock_fork' id='140240122195184'>
mock_logging_debug = <Mock name='mock_logging_debug' id='140240128747640'>
mock_map_type_to_command = <Mock name='mock_map_type_to_command' id='140240122785112'>
mocker     = <pytest_mock.MockFixture object at 0x7f8c329f07a8>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f8c329f0810>
self       = <tests.test_subprocess.TestScarlettSubprocess object at 0x7f8c32aaac20>
test_command = ['who', '-b']
test_fork  = False
test_name  = 'test_who'

tests/test_subprocess.py:267: Failed

 tests/test_subprocess.py::TestScarlettSubprocess.test_subprocess_check_command_type ⨯                                                           100% ██████████

НО!

Если я отфильтрую все остальные тесты, кроме проблемных, я получаю:

через py.test --pdb --showlocals -v -R : -k test_subprocess_check_command_type

[email protected]:~/dev/bossjones-github/scarlett_os$ py.test --pdb --showlocals -v -R : -k test_subprocess_check_command_type
/usr/local/lib/python3.5/site-packages/_pdbpp_path_hack/pdb.py:4: ResourceWarning: unclosed file <_io.TextIOWrapper name='/usr/local/lib/python3.5/site-packages/pdb.py' mode='r' encoding='UTF-8'>
  os.path.dirname(os.path.dirname(__file__)), 'pdb.py')).read(), os.path.join(
Test session starts (platform: linux, Python 3.5.2, pytest 3.0.7, pytest-sugar 0.8.0)
cachedir: .cache
benchmark: 3.1.0a2 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/pi/dev/bossjones-github/scarlett_os, inifile: setup.cfg
plugins: timeout-1.2.0, sugar-0.8.0, rerunfailures-2.1.0, mock-1.6.0, leaks-0.2.2, ipdb-0.1.dev2, cov-2.4.0, catchlog-1.2.2, benchmark-3.1.0a2
timeout: 60.0s method: signal
NOTE: DBUS_SESSION_BUS_ADDRESS environment var not found!
[DBUS_SESSION_BUS_ADDRESS]: unix:path=/tmp/dbus_proxy_outside_socket

 tests/test_subprocess.py::TestScarlettSubprocess.test_subprocess_check_command_type ✓                                                                                                                                                                           100% ██████████

Results (8.39s):
       1 passed
     190 deselected
[email protected]:~/dev/bossjones-github/scarlett_os$

Я также попытался вручную прокомментировать следующие 2 теста, и они позволили мне снова успешно запустить все тесты:

  • test_subprocess_init
  • test_subprocess_map_type_to_command

Может ли кто-нибудь увидеть что-то неладное с моей настройкой? Я читал несколько сообщений в блогах о том, "где насмехаться", и несколько раз смотрел на документы, не зная, чего я не вижу. https://docs.python.org/3/library/unittest.mock.html

Сведения о моей настройке

Здесь все, что может потребоваться для решения этой проблемы. Дайте мне знать, если мне нужно предоставить дополнительную информацию!

Также... пожалуйста, извините, как грязный мой код выглядит и все блоки комментариев. Я большой наблюдатель, когда узнаю что-то новое... В ближайшем будущем я сделаю все более питоническим и чистым:)

Мой код:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Scarlett Dbus Service. Implemented via MPRIS D-Bus Interface Specification."""

from __future__ import with_statement, division, absolute_import

import os
import sys
from scarlett_os.exceptions import SubProcessError
from scarlett_os.exceptions import TimeOutError
import logging
from scarlett_os.internal.gi import GObject
from scarlett_os.internal.gi import GLib

logger = logging.getLogger(__name__)


def check_pid(pid):
    """Check For the existence of a unix pid."""
    try:
        os.kill(pid, 0)
    except OSError:
        return False
    else:
        return True


class Subprocess(GObject.GObject):
    """
    GObject API for handling child processes.

    :param command: The command to be run as a subprocess.
    :param fork: If `True` this process will be detached from its parent and
                 run independent. This means that no excited-signal will be emited.

    :type command: `list`
    :type fork: `bool`
    """

    __gtype_name__ = 'Subprocess'
    __gsignals__ = {
        'exited': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_INT, GObject.TYPE_INT))
    }

    def __init__(self, command, name=None, fork=False, run_check_command=True):
        """Create instance of Subprocess."""

        GObject.GObject.__init__(self)

        self.process = None
        self.pid = None

        if not fork:
            self.stdout = True
            self.stderr = True
        else:
            self.stdout = False
            self.stderr = False

        self.forked = fork

        # Verify that command is properly formatted 
        # and each argument is of type str
        if run_check_command:
            self.check_command_type(command)

        self.command = command
        self.name = name

        logger.debug("command: {}".format(self.command))
        logger.debug("name: {}".format(self.name))
        logger.debug("forked: {}".format(self.forked))
        logger.debug("process: {}".format(self.process))
        logger.debug("pid: {}".format(self.pid))

        if fork:
            self.fork()

    # TODO: Add these arguments so we can toggle stdout
    # def spawn_command(self, standard_input=False, standard_output=False, standard_error=False):
    def spawn_command(self):
        # DO_NOT_REAP_CHILD
        # Don't reap process automatically so it is possible to detect when it is closed.
        return GLib.spawn_async(self.command,
                                flags=GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD
                                )

    def map_type_to_command(self, command):
        """Return: Map after applying type to several objects in an array"""
        # NOTE: In python3, many processes that iterate over iterables return iterators themselves. 
        # In most cases, this ends up saving memory, and should make things go faster.
        # cause of that, we need to call list() over the map object
        return list(map(type, command))

    def check_command_type(self, command):

        types = self.map_type_to_command(command)

        if type(types) is not list:
            raise TypeError("Variable types should return a list in python3. Got: {}".format(types))

        # NOTE: str is a built-in function (actually a class) which converts its argument to a string. 
        # string is a module which provides common string operations.
        # source: http://stackoverflow.com/info/2026038/relationship-between-string-module-and-str
        for t in types:
            if t is not str:
                raise TypeError("Executables and arguments must be str objects. types: {}".format(t))

        logger.debug("Running Command: %r" % " ".join(command))
        return True

    def run(self):
        """Run the process."""

        # NOTE: DO_NOT_REAP_CHILD: the child will not be automatically reaped;
        # you must use g_child_watch_add yourself (or call waitpid or handle `SIGCHLD` yourself),
        # or the child will become a zombie.
        # source:
        # http://valadoc.org/#!api=glib-2.0/GLib.SpawnFlags.DO_NOT_REAP_CHILD

        # NOTE: SEARCH_PATH: argv[0] need not be an absolute path, it will be looked for in the user PATH
        # source:
        # http://lazka.github.io/pgi-docs/#GLib-2.0/flags.html#GLib.SpawnFlags.SEARCH_PATH

        self.pid, self.stdin, self.stdout, self.stderr = self.spawn_command()

        logger.debug("command: {}".format(self.command))
        logger.debug("stdin: {}".format(self.stdin))
        logger.debug("stdout: {}".format(self.stdout))
        logger.debug("stderr: {}".format(self.stderr))
        logger.debug("pid: {}".format(self.pid))

        # close file descriptor
        self.pid.close()

        print(self.stderr)

        # NOTE: GLib.PRIORITY_HIGH = -100
        # Use this for high priority event sources.
        # It is not used within GLib or GTK+.
        watch = GLib.child_watch_add(GLib.PRIORITY_HIGH, 
                                     self.pid, 
                                     self.exited_cb)

        return self.pid

    def exited_cb(self, pid, condition):
        if not self.forked:
            self.emit('exited', pid, condition)

    def fork(self):
        """Fork the process."""
        try:
            # first fork
            pid = os.fork()
            if pid > 0:
                logger.debug('pid greater than 0 first time')
                sys.exit(0)
        except OSError as e:
            logger.error('Error forking process first time')
            sys.exit(1)

        # Change the current working directory to path.
        os.chdir("/")

        # Description: setsid() creates a new session if the calling process is not a process group leader. 
        # The calling process is the leader of the new session, 
        # the process group leader of the new process group, 
        # and has no controlling terminal. 
        # The process group ID and session ID of the calling process are set to the PID of the calling process. 
        # The calling process will be the only process in this new process group and in this new session.

        # Return Value: On success, the (new) session ID of the calling process is returned. 
        # On error, (pid_t) -1 is returned, and errno is set to indicate the error.
        os.setsid()

        # Set the current numeric umask and return the previous umask.
        os.umask(0)

        try:
            # second fork
            pid = os.fork()
            if pid > 0:
                logger.debug('pid greater than 0 second time')
                sys.exit(0)
        except OSError as e:
            logger.error('Error forking process second time')
            sys.exit(1)

Мой тест:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
test_subprocess
----------------------------------
"""

import os
import sys
import pytest

import scarlett_os
# import signal
# import builtins
# import re

class TestScarlettSubprocess(object):
    '''Units tests for Scarlett Subprocess, subclass of GObject.Gobject.'''

    def test_check_pid_os_error(self, mocker):
        # Feels like mocks are leaking into other tests, 
        # stop mock before starting each test function
        mocker.stopall()

        # Setup mock objects
        kill_mock = mocker.MagicMock(name=__name__ + "_kill_mock_OSError")
        kill_mock.side_effect = OSError

        # patch things
        mocker.patch.object(scarlett_os.subprocess.os, 'kill', kill_mock)

        # When OSError occurs, throw False
        assert not scarlett_os.subprocess.check_pid(4353634632623)
        # Verify that os.kill only called once
        assert kill_mock.call_count == 1

    def test_check_pid(self, mocker):
        # Feels like mocks are leaking into other tests, 
        # stop mock before starting each test function
        mocker.stopall()

        # Setup mock objects
        kill_mock = mocker.MagicMock(name=__name__ + "_kill_mock")

        mocker.patch.object(scarlett_os.subprocess.os, 'kill', kill_mock)

        result = scarlett_os.subprocess.check_pid(123)
        assert kill_mock.called
        # NOTE: test against signal 0
        # sending the signal 0 to a given PID just checks if any
        # process with the given PID is running and you have the
        # permission to send a signal to it.
        kill_mock.assert_called_once_with(123, 0)
        assert result is True

    # FIXME: I THINK THIS GUYS IS LEAKING MOCK OBJECTS
    def test_subprocess_init(self, mocker):
        # Feels like mocks are leaking into other tests, 
        # stop mock before starting each test function
        mocker.stopall()

        mock_check_command_type = MagicMock(name="mock_check_command_type")
        mock_check_command_type.return_value = True
        mock_fork = mocker.MagicMock(name="mock_fork")
        mock_logging_debug = mocker.MagicMock(name="mock_logging_debug")

        # mock
        mocker.patch.object(scarlett_os.subprocess.logging.Logger, 'debug', mock_logging_debug)
        mocker.patch.object(scarlett_os.subprocess.Subprocess, 'check_command_type', mock_check_command_type)
        mocker.patch.object(scarlett_os.subprocess.Subprocess, 'fork', mock_fork)

        # NOTE: On purpose this is an invalid cmd. Should be of type array
        test_command = ['who']

        test_name = 'test_who'
        test_fork = False

        s_test = scarlett_os.subprocess.Subprocess(test_command,
                                                   name=test_name,
                                                   fork=test_fork)

        # action
        assert s_test.check_command_type(test_command) is True
        mock_check_command_type.assert_called_with(['who'])
        assert not s_test.process
        assert not s_test.pid
        assert s_test.name == 'test_who'
        assert not s_test.forked
        assert s_test.stdout is True
        assert s_test.stderr is True

        mock_logging_debug.assert_any_call("command: ['who']")
        mock_logging_debug.assert_any_call("name: test_who")
        mock_logging_debug.assert_any_call("forked: False")
        mock_logging_debug.assert_any_call("process: None")
        mock_logging_debug.assert_any_call("pid: None")
        mock_fork.assert_not_called()

    # FIXME: I THINK THIS GUYS IS LEAKING MOCK OBJECTS
    def test_subprocess_map_type_to_command(self, mocker):
        """Using the mock.patch decorator (removes the need to import builtins)"""
        # Feels like mocks are leaking into other tests, 
        # stop mock before starting each test function
        mocker.stopall()

        mock_check_command_type = mocker.MagicMock(name="mock_check_command_type")
        mock_check_command_type.return_value = True
        mock_fork = mocker.MagicMock(name="mock_fork")
        mock_logging_debug = mocker.MagicMock(name="mock_logging_debug")

        # mock
        mocker.patch.object(scarlett_os.subprocess.logging.Logger, 'debug', mock_logging_debug)
        mocker.patch.object(scarlett_os.subprocess.Subprocess, 'check_command_type', mock_check_command_type)
        mocker.patch.object(scarlett_os.subprocess.Subprocess, 'fork', mock_fork)

        # NOTE: On purpose this is an invalid cmd. Should be of type array
        test_command = ["who", "-b"]
        test_name = 'test_who'
        test_fork = False

        # create subprocess object
        s_test = scarlett_os.subprocess.Subprocess(test_command,
                                                   name=test_name,
                                                   fork=test_fork)
        mocker.spy(s_test, 'map_type_to_command')
        assert isinstance(s_test.map_type_to_command(test_command), list)
        assert s_test.map_type_to_command.call_count == 1

        assert s_test.check_command_type(test_command)
        assert s_test.check_command_type(
            test_command) == mock_check_command_type.return_value

    def test_subprocess_check_command_type(self, mocker):
        """Using the mock.patch decorator (removes the need to import builtins)"""
        # Feels like mocks are leaking into other tests, 
        # stop mock before starting each test function
        mocker.stopall()

        test_command = ["who", "-b"]
        test_name = 'test_who'
        test_fork = False

        # mock
        mock_map_type_to_command = mocker.MagicMock(name="mock_map_type_to_command")
        # mock_map_type_to_command.return_value = int
        mock_map_type_to_command.side_effect = [int, [int, int]]
        mock_fork = mocker.MagicMock(name="mock_fork")
        mock_logging_debug = mocker.MagicMock(name="mock_logging_debug")

        mocker.patch.object(scarlett_os.subprocess.logging.Logger, 'debug', mock_logging_debug)
        mocker.patch.object(scarlett_os.subprocess.Subprocess, 'map_type_to_command', mock_map_type_to_command)
        mocker.patch.object(scarlett_os.subprocess.Subprocess, 'fork', mock_fork)


        # action
        with pytest.raises(TypeError) as excinfo:
            scarlett_os.subprocess.Subprocess(test_command,
                                              name=test_name,
                                              fork=test_fork,
                                              run_check_command=True)
        assert str(
            excinfo.value) == "Variable types should return a list in python3. Got: <class 'int'>"

        with pytest.raises(TypeError) as excinfo:
            scarlett_os.subprocess.Subprocess(test_command,
                                              name=test_name,
                                              fork=test_fork,
                                              run_check_command=True)

        assert str(
            excinfo.value) == "Executables and arguments must be str objects. types: <class 'int'>"

Моя структура папок (обратите внимание, что я удалил пару вещей, поскольку это было слишком многословно):

[email protected]:~/dev/bossjones-github/scarlett_os$ tree -I *.pyc
.
├── requirements_dev.txt
├── requirements_test_experimental.txt
├── requirements_test.txt
├── requirements.txt
├── scarlett_os
│   ├── automations
│   │   ├── __init__.py
│   │   └── __pycache__
│   ├── commands.py
│   ├── compat.py
│   ├── config.py
│   ├── const.py
│   ├── core.py
│   ├── emitter.py
│   ├── exceptions.py
│   ├── __init__.py
│   ├── internal
│   │   ├── debugger.py
│   │   ├── deps.py
│   │   ├── encoding.py
│   │   ├── formatting.py
│   │   ├── gi.py
│   │   ├── __init__.py
│   │   ├── path.py
│   │   ├── __pycache__
│   │   └── system_utils.py
│   ├── listener.py
│   ├── loader.py
│   ├── logger.py
│   ├── log.py
│   ├── __main__.py
│   ├── mpris.py
│   ├── player.py
│   ├── __pycache__
│   ├── receiver.py
│   ├── speaker.py
│   ├── subprocess.py
│   ├── tasker.py
│   ├── tools
│   │   ├── __init__.py
│   │   ├── package.py
│   │   ├── __pycache__
│   │   └── verify.py
│   └── utility
│       ├── audio.py
│       ├── dbus_runner.py
│       ├── dbus_utils.py
│       ├── distance.py
│       ├── dt.py
│       ├── file.py
│       ├── generators.py
│       ├── gnome.py
│       ├── __init__.py
│       ├── location.py
│       ├── __pycache__
│       ├── temperature.py
│       ├── threadmanager.py
│       ├── thread.py
│       ├── unit_system.py
│       └── yaml.py
├── setup.cfg
├── setup.py
├── tests
│   ├── common_integration.py
│   ├── common.py
│   ├── helpers
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   ├── test_config_validation.py
│   │   ├── test_entity.py
│   │   └── test_init.py
│   ├── __init__.py
│   ├── integration
│   │   ├── baseclass.py
│   │   ├── conftest.py
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   ├── README.md
│   │   ├── stubs.py
│   │   ├── test_integration_end_to_end.py
│   │   ├── test_integration_listener.py
│   │   ├── test_integration_mpris.py
│   │   ├── test_integration_player.py
│   │   ├── test_integration_tasker.py
│   │   ├── test_integration_tasker.py.enable_sound.diff
│   │   └── test_integration_threadmanager.py
│   ├── internal
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   ├── test_deps.py
│   │   ├── test_encoding.py
│   │   └── test_path.py
│   ├── performancetests
│   │   ├── baseclass.py
│   │   ├── __init__.py
│   │   └── __pycache__
│   ├── __pycache__
│   ├── run_all_tests
│   ├── run_dbus_tests.sh
│   ├── test_cli.py
│   ├── test_commands.py
│   ├── testing_config
│   │   └── custom_automations
│   │       ├── light
│   │       │   └── test.py
│   │       └── switch
│   │           └── test.py
│   ├── test_listener.py
│   ├── test_mpris.py
│   ├── test_player.py
│   ├── test_scarlett_os.py
│   ├── test_speaker.py
│   ├── test_subprocess.py
│   ├── test_tasker.py
│   ├── test_threadmanager.py
│   ├── tools_common.py
│   ├── unit_scarlett_os.py
│   └── utility
│       ├── __init__.py
│       ├── __pycache__
│       ├── test_dbus_utils.py
│       ├── test_distance.py
│       ├── test_dt.py
│       ├── test_gnome.py
│       ├── test_init.py
│       ├── test_location.py
│       ├── test_unit_system.py
│       └── test_yaml.py
67 directories, 256 files
[email protected]:~/dev/bossjones-github/scarlett_os$

Другие сведения (расширенный контроль застывания только в случае несовместимости):

# Python version
Python 3.5.2

# Pytest version ( and plugins )
pytest==3.0.7
pytest-benchmark==3.1.0a2
pytest-catchlog==1.2.2
pytest-cov==2.4.0
pytest-ipdb==0.1.dev2
pytest-leaks==0.2.2
pytest-mock==1.6.0
pytest-rerunfailures==2.1.0
pytest-sugar==0.8.0
pytest-timeout==1.2.0
python-dateutil==2.6.0
python-dbusmock==0.16.7


# Pip Freeze ( Just in case )
alabaster==0.7.10
appdirs==1.4.3
argh==0.26.2
asn1crypto==0.22.0
astroid==1.5.2
Babel==2.4.0
bleach==2.0.0
bumpversion==0.5.3
cffi==1.10.0
click==6.7
click-plugins==1.0.3
colorama==0.3.7
colorlog==2.10.0
coverage==4.3.4
coveralls==1.1
cryptography==1.8.1
Cython==0.25.2
decorator==4.0.11
docopt==0.6.2
docutils==0.13.1
ecdsa==0.13
entrypoints==0.2.2
Fabric3==1.12.post1
fancycompleter==0.7
fields==5.0.0
flake8==3.3.0
flake8-docstrings==1.0.3
flake8-polyfill==1.0.1
freezegun==0.3.8
gnureadline==6.3.3
graphviz==0.6
html5lib==0.999999999
hunter==1.4.1
idna==2.5
imagesize==0.7.1
ipdb==0.10.2
ipykernel==4.6.1
ipython==6.0.0
ipython-genutils==0.2.0
ipywidgets==6.0.0
isort==4.2.5
jedi==0.10.2
Jinja2==2.9.6
jsonschema==2.6.0
jupyter==1.0.0
jupyter-client==5.0.1
jupyter-console==5.1.0
jupyter-core==4.3.0
lazy-object-proxy==1.2.2
MarkupSafe==1.0
mccabe==0.6.1
mistune==0.7.4
mock==2.0.0
mock-open==1.3.1
mypy-lang==0.4.6
nbconvert==5.1.1
nbformat==4.3.0
notebook==5.0.0
objgraph==3.1.0
ordereddict==1.1
packaging==16.8
pandocfilters==1.4.1
paramiko==1.18.2
pathtools==0.1.2
pbr==1.10.0
pdbpp==0.8.3
pexpect==4.2.1
pickleshare==0.7.4
pluggy==0.4.0
plumbum==1.6.3
prompt-toolkit==1.0.14
psutil==5.2.2
ptyprocess==0.5.1
py==1.4.33
py-cpuinfo==3.2.0
pyasn1==0.2.3
pycodestyle==2.3.1
pycparser==2.17
pycrypto==2.6.1
pydbus==0.6.0
pydocstyle==2.0.0
pyflakes==1.5.0
pygal==2.3.1
pygaljs==1.0.1
Pygments==2.2.0
pygobject==3.22.0
pylint==1.7.1
pyparsing==2.2.0
pystuck==0.8.5
pytest==3.0.7
pytest-benchmark==3.1.0a2
pytest-catchlog==1.2.2
pytest-cov==2.4.0
pytest-ipdb==0.1.dev2
pytest-leaks==0.2.2
pytest-mock==1.6.0
pytest-rerunfailures==2.1.0
pytest-sugar==0.8.0
pytest-timeout==1.2.0
python-dateutil==2.6.0
python-dbusmock==0.16.7
pytz==2017.2
PyYAML==3.12
pyzmq==16.0.2
qtconsole==4.3.0
requests==2.13.0
requests-mock==1.3.0
rpyc==3.3.0
-e [email protected]:bossjones/[email protected]#egg=scarlett_os
simplegeneric==0.8.1
six==1.10.0
snowballstemmer==1.2.1
Sphinx==1.5.5
stevedore==1.18.0
termcolor==1.1.0
terminado==0.6
testpath==0.3
tornado==4.5.1
tox==2.7.0
traitlets==4.3.2
typing==3.6.1
virtualenv==15.0.3
virtualenv-clone==0.2.6
virtualenvwrapper==4.7.2
voluptuous==0.9.3
watchdog==0.8.3
wcwidth==0.1.7
webencodings==0.5.1
widgetsnbextension==2.0.0
wmctrl==0.3
wrapt==1.10.10
xdot==0.7

Изменить: (Еще одна деталь, почему я не просто менеджер контекста или декораторы)?

pytest-mock имеет довольно хороший раздел по их выбору дизайна и почему они решили отойти от вложенных операторов with и декораторов, сложенных друг на друга. Ссылка здесь, но позвольте мне упомянуть пару здесь на всякий случай:

- excessive nesting of with statements breaking the flow of test
- receiving the mocks as parameters doesn't mix nicely with pytest approach of naming fixtures as parameters, or pytest.mark.parametrize;

Итак, если можно сделать мой код немного более чистым с помощью этого плагина, я бы хотел, чтобы это произошло. Если это невозможно, то, возможно, мне нужно пересмотреть ситуацию.

Ответы

Ответ 1

Почему бы не запустить ваши mocks с декораторами функций или менеджерами контекста, чтобы убедиться, что они закрыты? Например, в test_subprocess_map_type_to_command,  вместо того чтобы делать все это, чтобы издеваться над scarlett_os.subprocess.Subprocess.check_command_type:

mock_check_command_type = mocker.MagicMock(name="mock_check_command_type")
mock_check_command_type.return_value = True
mocker.patch.object(scarlett_os.subprocess.Subprocess, 'check_command_type', mock_check_command_type)

Почему бы не просто использовать диспетчер контекста и сделать:

with mock.patch.object(
    scarlett_os.subprocess.Subprocess,
    'check_command_type',
    return_value=True):

Это будет много, и убедитесь, что ваш макет не течет.

Еще лучше, если ваши макеты применяются ко всей функции (я думаю, некоторые из них), вы можете использовать декоратор в верхней части функции:

@mock.patch('scarlett_os.subprocess.Subprocess.check_command_type', 
            return_value=True)

Ответ 2

Ошибка, которую вы получаете, заключается в том, что код под тестированием попал AttributeError вместо TypeError.

Деталь заключается в том, что предполагается, что какой-то объект имеет член .typename, и он этого не сделал.

Я подозреваю, как только вы разрешите эту загадку, остальное будет прекрасно.

Я вижу, что кто-то открыл https://github.com/pytest-dev/pytest-mock/issues/84 (вы?), подождите, пока разработчики pytest проанализируют его, если есть несовместимость между 2 плагинов.