I wanted to write unit tests for pytest fixtures present in conftest.py
How do I mock decorator pytest.fixture?
Conftest.py
import pytest
#pytest.fixture(scope="session", autouse="True")
def get_ip(dict_obj):
"""Assume some functionality"""
return dict_obj.get('ip')
#pytest.fixture(scope="class")
def get_server(create_obj):
"""Assume some functionality"""
pass
test_conftest.py
mock_fixture = patch('pytest.fixture', lambda x : x).start()
from tests.conftest import get_ip
class TestConftestTests:
def test_mgmt_ip(self):
assert mgmt_ip({"ip": "10.192.174.15"}) == "10.192.174.15"
E TypeError: <lambda>() got an unexpected keyword argument 'scope'
When I tried to mock pytest.fixture at the starting of the test module before importing functions to be tested, I am getting error -
E TypeError: <lambda>() got an unexpected keyword argument 'scope'
If I remove the lambda function, I am getting E AssertionError: assert <MagicMock name='fixture()()()' id='4443565456'> == '10.192.174.15'
test_conftest.py
patch('pytest.fixture').start()
from tests.conftest import get_ip
class TestConftestTests:
def test_mgmt_ip(self):
assert mgmt_ip({"ip": "10.192.174.15"}) == "10.192.174.15"
E AssertionError: assert <MagicMock name='fixture()()()' id='4443565456'> == '10.192.174.15'
Could someone help me to resolve the error ? Thanks!
#pytest.fixture(scope='session') is going to call your lambda x: x with the kwarg scope. A mock covering both the no-args and some-args versions of pytest.fixture might be:
pytest_fixture = lambda x=None, **kw: x if callable(x) else fixture
But it should be noted pytest.fixture doesn't do any registration by itself. It only marks the function as a fixture, by setting the _pytestfixturefunction attribute on it; afterward, pytest collects the fixture.
I'm not sure you're barking up the right tree for whatever you might be trying to accomplish. If you want to change what value a fixture has in certain contexts, classes and subclasses can be used to override parent fixtures. If you're trying to see whether a fixture is used, creating a new class and overriding the fixture with your assertion can be useful.
Related
Hello guys good afternoon, I hope everything is going well.
I have a project and of course locally works amazing without any problem. The issues comes when I push to GitLab and the CICD test_job doesn't pass so never is build.
Basically the problem is the test part.
The project has a conftest.py file that has some functions, like this:
def app(client_app):
f = partial(TestClient, client_app)
yield f
And the project has unit testing files, one example is like this:
import unittest
class MyTestApplicationTopic(unittest.TestCase):
URL = "/someUrl"
data_app_topic = {
"topic": "burning",
"topic_remarks": "burning",
"application_id": 1
}
def test_connection(self, app):
with app() as client:
response = client.get(self.URL)
self.assertEqual(response.status_code, 200)
print('test passed')
if __name__ == '__main__':
tester = MyTestApplicationTopic()
tester.test_connection()
So basically as you see it's a normal unit test.
As you see the app() function from conftest.py is passed to the test_connection(self, app) function as argument
Of course this is not going to work because when I initialized the class I don't pass the argument:
if __name__ == '__main__':
tester = MyTestApplicationTopic()
tester.test_connection()
The thing is that I don't know how to pass the argument because as I mentioned before, the tes_conecction(self, app) function, takes this argument from the conftest.py.
So, Obviusly I get the error:
'TypeError: test_connection() missing 1 required positional argument: 'app'
So basically is something confuse for me because I need to pass the app from conftest.py because this is something related with the project configuration so bascially I pass app() function as argument but also the app()function takes another fucntios as argument and that fucntion takes another function as argument and so on and so for.
So at the end I don't know how handle the error: 'TypeError: test_connection() missing 1 required positional argument: 'app' in this specific scenario.
conftest.py seems to be for pytest, not unittest. Since pytest can run unittest tests, you can run tests with Pytest and put the fixture decorator on app:
#pytest.fixture
def app(client_app):
f = partial(TestClient, client_app)
yield f
With pytest, you don't need the if main, just run pytest and the framework will run tests for you while injecting fixture when needed.
path/to/project$ pytest
learning python mocks here. I need some helps to understand how the patch work when mocking a class.
In the code below, I mocked a class. the function under tests receives the mock and calls a function on it. In my assertions, the class is successfully called, but the function is reported as not being called.
I added a debug print to view the content in the function under tests and it is reported as called.
My expectation is the assertion assert facadeMock.install.called should be true. Why is it not reported as called and how do I achieve this?
Thank you.
install/__init__.py
from .facade import Facade
def main():
f = Facade()
f.install()
print('jf-debug-> "f.install.called": {value}'.format(
value=f.install.called))
test/install_tests.py
import os
import sys
# allow import of package
sys.path.insert(0,
os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from unittest.mock import patch
import install
#patch('install.Facade') # using autospec=True did not change the result
def test_main_with_links_should_call_facade_install_with_link_true(facadeMock):
install.main()
assert facadeMock.called
assert facadeMock.install is install.Facade.install
assert facadeMock.install.called # <-------------------- Fails here!
output:
============================= test session starts ==============================
platform linux -- Python 3.10.6, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/jfl/ubuntu-vim, configfile: pytest.ini
collected 1 item
test/install_tests.py jf-debug-> "f.install.called": True
F
=================================== FAILURES ===================================
________ test_main_with_links_should_call_facade_install_with_link_true ________
facadeMock = <MagicMock name='Facade' id='140679041900864'>
#patch('install.Facade')
def test_main_with_links_should_call_facade_install_with_link_true(facadeMock):
install.main()
assert facadeMock.called
assert facadeMock.install is install.Facade.install
> assert facadeMock.install.called
E AssertionError: assert False
E + where False = <MagicMock name='Facade.install' id='140679042325216'>.called
E + where <MagicMock name='Facade.install' id='140679042325216'> = <MagicMock name='Facade' id='140679041900864'>.install
test/install_tests.py:21: AssertionError
=========================== short test summary info ============================
FAILED test/install_tests.py::test_main_with_links_should_call_facade_install_with_link_true - AssertionError: assert False
============================== 1 failed in 0.09s ===============================
[edit]
Thank you to #chepner and #Daniil Fajnberg for their comments. I found the cause of the problem.
The problem can be reduced at:
install/__init__.py receives an instance of Facade when calling Facade() in main().
This instance is not the same as the one received in parameters of the test. They are different instances.
to retrieve the instance received in main(), do:
actualInstance = facadeMock.return_value
assert actualInstance.install.called
And it works!
Thank you. That really helps me understand the working of mocks in python.
[/edit]
I have found a method to solve your problem; it is empirical but it works.
To pass your test I have modified it as you can see below:
#patch('install.Facade') # using autospec=True did not change the result
def test_main_with_links_should_call_facade_install_with_link_true(facadeMock):
install.main()
assert facadeMock.called
assert facadeMock.install is install.Facade.install
#assert facadeMock.install.called # <-------------------- Fails here!
install_called = False
for call_elem in facadeMock.mock_calls:
if call_elem[0] == "().install":
install_called = True
break
assert install_called == True
Mock objects facadeMock and f are distinct
facadeMock is a mock object created in the test code and it is used by the production code during your test to create the mock object f by the instruction:
f = Facade()
In the production code f is a mock object (that is an instance of the class Mock) because it is created by the Mock object Facade that is exactly facadeMock.
But f and facadeMock are 2 different instances of the class Mock.
Below I show the id values of facadeMock, Facade and f:
facadeMock = <MagicMock name='Facade' id='140449467990536'>
Facade = <MagicMock name='Facade' id='140449467990536'>
f = <MagicMock name='Facade()' id='140449465274608'>
The id for facadeMock, Facade but are different from the id of f.
The attribute mock_calls
When your test code is executed the function install.main() execution causes the definition of the attribute mock_calls for the mock object facadeMock.
This attribute is a list of complex elements.
If you check the first field (I mean the field in position 0) of each one of this element you can find the name of the methods of the mock that are called.
In your case you have to found install and to do this you have to look for ().install.
So my test checks all the element of mock_calls and only if ().install is found set the variable install_called=True.
I hope that this answer can help you.
I have been reading into python mocking but can't get my head around why the following code is failing.
I have two classes, a Potato and a PotatoBag like the following. Figure is stored in food.py and Report is stored in bag.py.
class Potato:
def create_potato(self):
pass
def output_potato(self):
pass
class PotatoBag:
def __init__(self, potatoes):
self.potatoes = potatoes
def output_to_file(self):
for fig in self.potatoes:
fig.create_potato()
fig.output_potato()
Currently I am trying to unit test the output method so that Report correctly calls create_figure and output_figure from Figure using a mock. This is my test code:
from unittest.mock import MagicMock, patch
from bag import PotatoBag
from food import Potato
import pytest
#pytest.fixture(scope='module')
def potatoes():
x = Potato()
y = Potato()
return [x, y]
#patch('food.Potato')
def test_output_to_file(mock_potato, potatoes):
test_potato_bag = PotatoBag(potatoes)
test_potato_bag.output_to_file()
mock_potato.return_value.create_potato.assert_called()
mock_potato.return_value.output_potato.assert_called()
Immediately pytest yields an AssertionError stating that create_figure was never called.
_mock_self = <MagicMock name='Potato().create_potato' id='140480853451272'>
def assert_called(_mock_self):
"""assert that the mock was called at least once
"""
self = _mock_self
if self.call_count == 0:
msg = ("Expected '%s' to have been called." %
self._mock_name or 'mock')
> raise AssertionError(msg)
E AssertionError: Expected 'create_potato' to have been called.
/home/anaconda3/lib/python3.7/unittest/mock.py:792: AssertionError
What is wrong with my code?
You are passing the Report a list of Figures from your fixture instead of a mock.
Changing your test to...
#patch('figure.Figure')
def test_output_to_file(mock_figure, figures):
test_report = Report([mock_figure])
test_report.output_to_file()
mock_figure.create_figure.assert_called_once()
mock_figure.output_figure.assert_called_once()
This resolves testing that output_to_file correctly is calling the functions on Figure without actually worrying about setting up a figure and dealing with any side effects or additional complexities that may come with calling those functions. The worries of that can be saved for the unit tests for Figure ;)
I am trying to use PyTest_Mock in order to do some testing in my Python project. I created a very simple test to try it out, but I am getting an AttributeError and I don't know why.
model.py
def square(x):
return x * x
if __name__ == '__main__':
res = square(5)
print("result: {}".format(res))
test_model.py
import pytest
from pytest_mock import mocker
import model
def test_model():
mocker.patch(square(5))
assert model.square(5) == 25
After running python -m pytest I get a failure and the following error:
def test_model():
> mocker.patch(square(5))
E AttributeError: 'function' object has no attribute 'patch'
test_model.py:7: AttributeError
You don't need to import mocker, it's available as fixture, so you just pass it as a parameter in the test function:
def test_model(mocker):
mocker.patch(...)
square(5) evaluates to 25, so mocker.patch(square(5)) will effectively try to patch a number 25. Instead, pass the function name as parameter: either
mocker.patch('model.square')
or
mocker.patch.object(model, 'square')
Once patched, square(5) will not return 25 anymore since the original function is replaced with a mock object that can return anything and will return a new mock object by default. assert model.square(5) == 25 will thus fail. Usually, you patch stuff either to avoid complex test setup or simulate behaviour of components that is desired in test scenario (for example, a website being unavailable). In your example, you don't need mocking at all.
Complete working example:
import model
def test_model(mocker):
mocker.patch.object(model, 'square', return_value='foo')
assert model.square(5) == 'foo'
src/mainDir/mainFile.py
contents of mainFile.py
import src.tempDir.tempFile as temp
data = 'someData'
def foo(self):
ans = temp.boo(data)
return ans
src/tempDir/tempFile.py
def boo(data):
ans = data
return ans
Now I want to test foo() from src/tests/test_mainFile.py and I want to mock temp.boo(data) method in foo() method
import src.mainDir.mainFile as mainFunc
testData = 'testData'
def test_foo(monkeypatch):
monkeypatch.setattr('src.tempDir.tempFile', 'boo', testData)
ans = mainFunc.foo()
assert ans == testData
but I get error
AttributeError: 'src.tempDir.tempFile' has no attribute 'boo'
I expect ans = testData.
I would like to know if I am correctly mocking my tempDir.boo() method or I should use pytest's mocker instead of monkeypatch.
You're telling monkeypatch to patch the attribute boo of the string object you pass in.
You'll either need to pass in a module like monkeypatch.setattr(tempFile, 'boo', testData), or pass the attribute as a string too (using the two-argument form), like monkeypatch.setattr('src.tempDir.tempFile.boo', testData).
My use case was was slightly different but should still apply. I wanted to patch the value of sys.frozen which is set when running an application bundled by something like Pyinstaller. Otherwise, the attribute does not exist. Looking through the pytest docs, the raising kwarg controls wether or not AttributeError is raised when the attribute does not already exist. (docs)
Usage Example
import sys
def test_frozen_func(monkeypatch):
monkeypatch.setattr(sys, 'frozen', True, raising=False)
# can use ('fq_import_path.sys.frozen', ...)
# if what you are trying to patch is imported in another file
assert sys.frozen
Update: mocking function calls can be done with monkeypatch.setattr('package.main.slow_fun', lambda: False) (see answer and comments in https://stackoverflow.com/a/44666743/3219667) and updated snippet below
I don't think this can be done with pytest's monkeypatch, but you can use the pytest-mock package. Docs: https://github.com/pytest-dev/pytest-mock
Quick example with the two files below:
# package/main.py
def slow_fun():
return True
def main_fun():
if slow_fun():
raise RuntimeError('Slow func returned True')
# tests/test_main.py
from package.main import main_fun
# Make sure to install pytest-mock so that the mocker argument is available
def test_main_fun(mocker):
mocker.patch('package.main.slow_fun', lambda: False)
main_fun()
# UPDATE: Alternative with monkeypatch
def test_main_fun_monkeypatch(monkeypatch):
monkeypatch.setattr('package.main.slow_fun', lambda: False)
main_fun()
Note: this also works if the functions are in different files