Home > Mobile >  Dealing with MagicMock Conversions in Pytest
Dealing with MagicMock Conversions in Pytest

Time:09-24

The context is I'm trying to unit test some Python code that involves a Python version being checked for. I had this working under the platform approach, but I was told using platform is not a great idea and I should use sys.version_info instead. Thus I have the following logic:

def main() -> None:
  python_version = f"{sys.version_info[0]}.{sys.version_info[1]}"

  if float(python_version) < 3.7:
     sys.stderr.write("\nQuendor requires Python 3.7 or later.\n")
     sys.stderr.write(f"Your current version is {python_version}\n\n")
     sys.exit(1)

(I do realize I could also do if sys.version_info < (3, 7): for my check.)

My unit test is this:

def test_bad_python_version(capsys) -> None:
    import sys
    from quendor.__main__ import main

    with mock.patch.object(sys, "version_info") as v_info:
        v_info.major = 3
        v_info.minor = 5

        main()

    terminal_text = capsys.readouterr()
    print(terminal_text)

Here my test isn't complete, of course. I'm trying to print the output just to make sure the test is working.

Running that test gets me this:

ValueError: could not convert string to float:
"<MagicMock name='version_info.__getitem__()'
id='1417904973808'>.<MagicMock name='version_info.__getitem__()'
id='1417904973808'>"

Incidentally, if my switch my condition in main to this (if sys.version_info < (3, 7):) I get a similar but different error:

TypeError: '<' not supported between instances of 'MagicMock' and 'tuple'

I'm not sure how to get the version_info passed in correctly, which I think is my issue here. I have searched the documentation and I've seen other questions here (example: How to unit test a python version switch) about MagicMock but nothing that just definitively states: "This is how to do this."

I should note that I had a version working perfectly fine when using platform. That version of my test looked like this:

with mock.patch.object(
        platform,
        "python_version",
        return_value="3.5",
    ), pytest.raises(SystemExit) as pytest_wrapped_e:
        main()

    terminal_text = capsys.readouterr()
    expect(terminal_text.err).to(contain("Quendor requires Python 3.7"))

Key to that seemed to be the "return_value" I was using.

CodePudding user response:

In your unit test you are setting/mocking the version by doing

v_info.major = 3
v_info.minor = 5

but accessing sys.version_info[0] or sys.version_info[1].

Try to change your version extraction in main() to

python_version = f"{sys.version_info.major}.{sys.version_info.minor}"

Worked for me:

In [8]: with mock.patch.object(sys, "version_info") as v_info:
   ...:     v_info.major = 3
   ...:     v_info.minor = 5
   ...:     main()
   ...: 

Quendor requires Python 3.7 or later.
Your current version is 3.5

CodePudding user response:

You are patching it this way:

with mock.patch.object(sys, "version_info") as v_info:
    v_info.major = 3
    v_info.minor = 5

Thus, it will be applied to the following calls:

sys.version_info.major  # Will display 3
sys.version_info.minor  # Will display 5

But instead, what you accessed in the source code was different:

sys.version_info
sys.version_info[0]
sys.version_info[1]

Obviously, your patches wouldn't reflect because they are targeted to .major and .minor but those aren't called anyways. Instead, change the patch to be applied to sys.version_info which should return a tuple of items (3, 5) so that it would reflect since it's the one called in your source code. No need to change anything from the source code:

src.py

import sys


def main() -> None:
    print("Call main")
    python_version = f"{sys.version_info[0]}.{sys.version_info[1]}"

    if float(python_version) < 3.7:
        print("Quendor requires Python 3.7 or later.")


def main2() -> None:
    print("Call main2")
    if sys.version_info < (3, 7):
        print("Quendor requires Python 3.7 or later.")

test_src.py

import sys
from unittest import mock

from src import main, main2


def test_bad_python_version():
    with mock.patch.object(sys, "version_info", (3, 5)) as v_info:
        main()
        main2()

def test_good_python_version():
    with mock.patch.object(sys, "version_info", (3, 8)) as v_info:
        main()
        main2()

Output

$ pytest -q -rP
..                                                                                            [100%]
============================================== PASSES ===============================================
______________________________________ test_bad_python_version ______________________________________
--------------------------------------- Captured stdout call ----------------------------------------
Call main
Quendor requires Python 3.7 or later.
Call main2
Quendor requires Python 3.7 or later.
_____________________________________ test_good_python_version ______________________________________
--------------------------------------- Captured stdout call ----------------------------------------
Call main
Call main2
2 passed in 0.06s
  • Related