2

I'm having trouble with pytest-mock and mocking open.

The code I wish to test looks like:

import re
import os

def get_uid():
    regex = re.compile('Serial\s+:\s*(\w+)')
    uid = "NOT_DEFINED"
    exists = os.path.isfile('/proc/cpuinfo')
    if exists:
        with open('/proc/cpuinfo', 'r') as file:
            cpu_details = file.read()
            uid = regex.search(cpu_details).group(1)
    return uid

So the test file is:

import os
import pytest

from cpu_info import uid

@pytest.mark.usefixtures("mocker")
class TestCPUInfo(object):
    def test_no_proc_cpuinfo_file(self):
        mocker.patch(os.path.isfile).return_value(False)
        result = uid.get_uid()
        assert result == "NOT_FOUND"

    def test_no_cpu_info_in_file(self):
        file_data = """
Hardware    : BCM2835
Revision    : a020d3
        """
        mocker.patch('__builtin__.open', mock_open(read_data=file_data))
        result = uid.get_uid()
        assert result == "NOT_DEFINED"

    def test_cpu_info(self):
        file_data = """
Hardware    : BCM2835
Revision    : a020d3
Serial      : 00000000e54cf3fa
        """
        mocker.patch('__builtin__.open', mock_open(read_data=file_data))
        result = uid.get_uid()
        assert result == "00000000e54cf3fa"

The test run gives:

pytest
======================================= test session starts ========================================
platform linux -- Python 3.5.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /home/robertpostill/software/gateway
plugins: mock-1.10.4
collected 3 items

cpu_info/test_cpu_info.py FFF                                                                [100%]

============================================= FAILURES =============================================
______________________________ TestCPUInfo.test_no_proc_cpuingo_file _______________________________

self = <test_cpu_info.TestCPUInfo object at 0x75e6eaf0>

    def test_no_proc_cpuingo_file(self):
>       mocker.patch(os.path.isfile).return_value(False)
E       NameError: name 'mocker' is not defined

cpu_info/test_cpu_info.py:9: NameError
___________________________________ TestCPUInfo.test_no_cpu_info ___________________________________

self = <test_cpu_info.TestCPUInfo object at 0x75e69d70>

        def test_no_cpu_info(self):
            file_data = """
    Hardware    : BCM2835
    Revision    : a020d3
            """
>           mocker.patch('__builtin__.open', mock_open(read_data=file_data))
E           NameError: name 'mocker' is not defined

cpu_info/test_cpu_info.py:18: NameError
____________________________________ TestCPUInfo.test_cpu_info _____________________________________

self = <test_cpu_info.TestCPUInfo object at 0x75e694f0>

        def test_cpu_info(self):
            file_data = """
    Hardware    : BCM2835
    Revision    : a020d3
    Serial      : 00000000e54cf3fa
            """
>           mocker.patch('__builtin__.open', mock_open(read_data=file_data))
E           NameError: name 'mocker' is not defined

cpu_info/test_cpu_info.py:28: NameError
===================================== 3 failed in 0.36 seconds =====================================

I think I've declared the mocker fixture correctly but it would seem not... What am I doing wrong?

robertpostill
  • 3,820
  • 3
  • 29
  • 38
  • 1
    `mark.usefixtures` will only ensure the mentioned fixtures will execute; it will not load `mocker` in the global namespace. In general, you don't access fixture return value like that - include fixture name in the args list, e.g. `def test_no_proc_cpuinfo_file(self, mocker): ...`. `pytest` will replace these args with correct fixture values at runtime. – hoefling Apr 27 '19 at 08:47
  • I'm totally open to the idea that I'm doing this wrong. I just seem to be going round in circles of blog posts that don't provide any real illumination as to how I should go about testing this with mocks. – robertpostill Apr 27 '19 at 13:47
  • There is a decent answer below, is there a reason for you not to accept that? – Yaroslav Nikitenko Jan 17 '21 at 10:41

1 Answers1

6

There are not that many issues with mock usage in your tests. In fact, there are only two:

Accessing mocker fixture

If you need to access the return value of a fixture, include its name in the test function arguments, for example:

class TestCPUInfo:
    def test_no_proc_cpuinfo_file(self, mocker):
        mocker.patch(...)

pytest will automatically map the test argument value to fixture value when running the tests.

Using mocker.patch

mocker.patch is just a shim to unittest.mock.patch, nothing more; it's there merely for convenience so that you don't have to import unittest.mock.patch everywhere. This means that mocker.patch has the same signature as unittest.mock.patch and you can always consult the stdlib's docs when in doubt of using it correctly.

In you case, mocker.patch(os.path.isfile).return_value(False) is not a correct usage of patch method. From the docs:

target should be a string in the form 'package.module.ClassName'.

...

patch() takes arbitrary keyword arguments. These will be passed to the Mock (or new_callable) on construction.

This means that the line

mocker.patch(os.path.isfile).return_value(False)

should be

mocker.patch('os.path.isfile', return_value=False)

Discrepancies between tested behaviour and real implementation logic

All that is left now are errors that have something to do with your implementation; you have to either adapt the tests to test the correct behaviour or fix the implementation errors.

Examples:

assert result == "NOT_FOUND"

will always raise because "NOT_FOUND" isn't even present in the code.

assert result == "NOT_DEFINED"

will always raise because uid = "NOT_DEFINED" will always be overwritten with regex search result and thus never returned.

Working example

Assuming your tests are the single source of truth, I fixed two errors with mock usage described above and adapted the implementation of get_uid() to make the tests pass:

import os
import re

def get_uid():
    regex = re.compile(r'Serial\s+:\s*(\w+)')
    exists = os.path.isfile('/proc/cpuinfo')
    if not exists:
        return 'NOT_FOUND'
    with open('/proc/cpuinfo', 'r') as file:
        cpu_details = file.read()
        match = regex.search(cpu_details)
        if match is None:
            return 'NOT_DEFINED'
        return match.group(1)

Tests:

import pytest
import uid


class TestCPUInfo:

    def test_no_proc_cpuinfo_file(self, mocker):
        mocker.patch('os.path.isfile', return_value=False)
        result = uid.get_uid()
        assert result == "NOT_FOUND"

    def test_no_cpu_info_in_file(self, mocker):
        file_data = """
Hardware    : BCM2835
Revision    : a020d3
        """
    mocker.patch('builtins.open', mocker.mock_open(read_data=file_data))
        result = uid.get_uid()
        assert result == "NOT_DEFINED"

    def test_cpu_info(self, mocker):
        file_data = """
Hardware    : BCM2835
Revision    : a020d3
Serial      : 00000000e54cf3fa
        """
    mocker.patch('builtins.open', mocker.mock_open(read_data=file_data))
        result = uid.get_uid()
        assert result == "00000000e54cf3fa"

Note that I'm using Python 3, so I can't patch __builtin__ and resort to patching builtins; aside from that, the code should be identical to Python 2 variant. Also, since mocker is used anyway, I used mocker.mock_open, thus saving me the additional import of unittest.mock.mock_open.

hoefling
  • 59,418
  • 12
  • 147
  • 194
  • 1
    This is a phenomenal answer. Definitely leaves me believing that there's no sane reason that `mocker` is missing as a positional argument when `_callTestMethod(self, method)` attempts to call my `test_foo(self, mocker)` method. Too bad it's complaining about the absence of `mocker` anyway :/ – interestedparty333 Nov 11 '20 at 22:39
  • As a follow-up, Test classes derived from `TestCase` with member tests don't really support fixtures like this needs. Deriving from `object` instead is suggested. Also see https://github.com/pytest-dev/pytest-mock/issues/174 – interestedparty333 Nov 11 '20 at 23:05
  • @interestedparty333 sorry for the late reply, didn't see your comments right away. Yeah, `pytest` features almost aren't supported with `unittest`-style tests; it is possiblt to inject fixtures via an autouse fixture (see [my other answer](https://stackoverflow.com/a/50135020/2650249)), but no fixtures as test method args. – hoefling Jan 18 '21 at 12:34
  • @interestedparty333 I see there's an example of injecting fixtures in the linked GH issue already, disregard my comment. – hoefling Jan 18 '21 at 12:36