Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

pytest-mocks and declaring a class level fixture

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?

like image 518
robertpostill Avatar asked Oct 15 '25 18:10

robertpostill


1 Answers

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.

like image 145
hoefling Avatar answered Oct 17 '25 08:10

hoefling