I'm using pytest and I have 5+ tests that have the exactly same first five lines. Is it possible to create a setup function for the repeated code?
#mock.patch('abcde.xyz')
def test_1(mocked_function):
x_mock = mock.Mock(X)
x_mock.producer = mock.Mock()
x_mock.producer.func2 = lambda : None
mocked_function.return_value = x_mock # xyz() returns x_mock
......
#mock.patch('abcde.xyz')
def test_2(mocked_function):
x_mock = mock.Mock(X)
x_mock.producer = mock.Mock()
x_mock.producer.func2 = lambda : None
mocked_function.return_value = x_mock
......
#mock..... # more
You should consider using a fixture as it recommended over classic setup/teardown methods.
From pytest documentation:
While these setup/teardown methods are simple and familiar to those coming from a unittest or nose background, you may also consider using pytest’s more powerful fixture mechanism which leverages the concept of dependency injection, allowing for a more modular and more scalable approach for managing test state, especially for larger projects and for functional testing.
For your example - considering that mocked_function is itself a fixture - it would be:
#pytest.fixture()
def mock_abcde_xyz(mocker):
mocker.patch("abcde.xyz")
#pytest.fixture()
#pytest.mark.usefixtures("mock_abcde_xyz")
def patched_mocked_function(mocker, mocked_function):
x_mock = mocker.Mock(X)
x_mock.producer = mocker.Mock()
x_mock.producer.func2 = lambda : None
mocked_function.return_value = x_mock
return mocked_function
def test_1(patched_mocked_function):
......
def test_2(patched_mocked_function):
......
Note that I used pytest-mock instead of mock so that you can use 'mocker' fixture.
If you don't want pytest-mock, just do:
#pytest.fixture()
#mock.patch('abcde.xyz')
def patched_mocked_function(mocked_function):
x_mock = mock.Mock(X)
x_mock.producer = mock.Mock()
x_mock.producer.func2 = lambda : None
mocked_function.return_value = x_mock
return mocked_function
def test_1(patched_mocked_function):
......
def test_2(patched_mocked_function):
......
Yes, you can implement a setUp() (or setUpClass()) method.
Another way would be to implement a helper function, as you would in any other Python code.
Related
End goal: I want to be able to quickly mock the input() built-in function in pytest, and replace it with an iterator that generates a (variable) list of strings. This is my current version, which works:
from typing import Callable
import pytest
def _create_patched_input(str_list: list[str]) -> Callable:
str_iter = iter(str_list.copy())
def patched_input(prompt: str) -> str: # has the same signature as input
val = next(str_iter)
print(prompt + val, end="\n"),
return val
return patched_input
#pytest.fixture
def _mock_input(monkeypatch, input_string_list: list[str]):
patched_input = _create_patched_input(input_string_list)
monkeypatch.setattr("builtins.input", patched_input)
def mock_input(f):
return pytest.mark.usefixtures("_mock_input")(f)
# Beginning of test code
def get_name(prompt: str) -> str:
return input(prompt)
#mock_input
#pytest.mark.parametrize(
"input_string_list",
(["Alice", "Bob", "Carol"], ["Dale", "Evie", "Frank", "George"]),
)
def test_get_name(input_string_list):
for name in input_string_list:
assert get_name("What is your name?") == name
However, this feels incomplete for a few reasons:
It requires the parameter name in the parameterize call to be input_string_list, which feels brittle.
If I move the fixtures into another function, I need to import both mock_input and _mock_input.
What would feel correct to me is to have a decorator (factory) that can be used like #mock_input(strings), such that you could use it like
#mock_input(["Alice", "Bob", "Carol"])
def test_get_name():
....
or, more in line with my use case,
#pytest.mark.parametrize(
"input_list", # can be named whatever
(["Alice", "Bob", "Carol"], ["Dale", "Evie", "Frank", "George"]),
)
#mock_input(input_list)
def test_get_name():
....
The latter I don't think you can do, as pytest wont recognize it as a fixture. What's the best way to do this?
I'd use indirect parametrization for mock_input, since it cannot work without receiving parameters. Also, I would refactor mock_input into a fixture that does passing through the arguments it receives, performing the mocking on the way. For example, when using unittest.mock.patch():
import pytest
from unittest.mock import patch
#pytest.fixture
def inputs(request):
texts = requests.param # ["Alice", "Bob", "Carol"] etc
with patch('builtins.input', side_effect=texts):
yield texts
Or, if you want to use monkeypatch, the code gets a bit more complex:
#pytest.fixture
def inputs(monkeypatch, request):
texts = requests.param
it = iter(texts)
def fake_input(prefix):
return next(it)
monkeypatch.setattr('builtins.input', fake_input)
yield texts
Now use inputs as test argument and parametrize it indirectly:
#pytest.mark.parametrize(
'inputs',
(["Alice", "Bob", "Carol"], ["Dale", "Evie", "Frank", "George"]),
indirect=True
)
def test_get_name(inputs):
for name in inputs:
assert get_name("What is your name?") == name
I had created a simple example to illustrate my issue. First is the setup say mydummy.py:
class TstObj:
def __init__(self, name):
self.name = name
def search(self):
return self.name
MyData = {}
MyData["object1"] = TstObj("object1")
MyData["object2"] = TstObj("object2")
MyData["object3"] = TstObj("object3")
def getObject1Data():
return MyData["object1"].search()
def getObject2Data():
return MyData["object2"].search()
def getObject3Data():
return MyData["object3"].search()
def getExample():
res = f"{getObject1Data()}{getObject2Data()}{getObject3Data()}"
return res
Here is the test that failed.
def test_get_dummy1():
dummy.MyData = MagicMock()
mydummy.MyData["object1"].search.side_effect = ["obj1"]
mydummy.MyData["object2"].search.side_effect = ["obj2"]
mydummy.MyData["object3"].search.side_effect = ["obj3"]
assert mydummy.getExample() == "obj1obj2obj3"
The above failed with run time error:
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py:1078: StopIteration
Here is the test that passed:
def test_get_dummy2():
dummy.MyData = MagicMock()
mydummy.MyData["object1"].search.side_effect = ["obj1", "obj2", "obj3"]
assert mydummy.getExample() == "obj1obj2obj3"
Am I missing something? I would have expected test_get_dummy1() to work and test_get_dummy2() to fail and not vice versa. Where and how can I find/learn more information about mocking to explain what is going on...
MyData["object1"] is converted to this function call: MyData.__getitem__("object1"). When you call your getExample method, the __getitem__ method is called 3 times with 3 parameters ("object1", "object2", "object3").
To mock the behavior you could have written your test like so:
def test_get_dummy_alternative():
mydummy.MyData = MagicMock()
mydummy.MyData.__getitem__.return_value.search.side_effect = ["obj1", "obj2", "obj3"]
assert mydummy.getExample() == "obj1obj2obj3"
Note the small change from your version: mydummy.MyData["object1"]... became: mydummy.MyData.__getitem__.return_value.... This is the regular MagicMock syntax - we want to to change the return value of the __getitem__ method.
BONUS:
I often struggle with mock syntax and understanding what's happening under the hood. This is why I wrote a helper library: the pytest-mock-generator. It can show you the actual calls made to the mock object.
To use it in your case you could have added this "exploration test":
def test_get_dummy_explore(mg):
mydummy.MyData = MagicMock()
mydummy.getExample()
mg.generate_asserts(mydummy.MyData, name='mydummy.MyData')
When you execute this test, the following output is printed to the console, which contains all the asserts to the actual calls to the mock:
from mock import call
mydummy.MyData.__getitem__.assert_has_calls(calls=[call('object1'),call('object2'),call('object3'),])
mydummy.MyData.__getitem__.return_value.search.assert_has_calls(calls=[call(),call(),call(),])
mydummy.MyData.__getitem__.return_value.search.return_value.__str__.assert_has_calls(calls=[call(),call(),call(),])
You can easily derive from here what has to be mocked.
I have a test class with a parametrization in all methods. I also want to parametrize some methods with other variables. However, I do not want to run all combination (as some of them will always fail).
Consider the following:
#pytest.mark.parametrize("base_url", ["api/v1/actions/", "api/v1/quotes/"])
class TestAPIResponse:
#pytest.mark.parametrize("api_verbose_name", ["ação", "declaração"])
def test_example(self, api_client, base_url, api_verbose_name):
...
With this configuration we have 4 tests:
base_url = "api/v1/actions/" and api_verbose_name = "ação"
base_url = "api/v1/quotes/" and api_verbose_name = "declaração"
base_url = "api/v1/actions/" and api_verbose_name = "declaração"
base_url = "api/v1/quotes/" and api_verbose_name = "ação"
How can I make test_example run only the 3rd and 4th test described above?
Right now I'm doing getting the api_verbose_name through a helper function...
def get_api_verbose_name(base_url: str) -> str:
if "quotes" in base_url:
api_verbose_name = "declaração"
if "actions" in base_url:
api_verbose_name = "ação"
return api_verbose_name
#pytest.mark.parametrize("base_url", ["api/v1/actions/", "api/v1/quotes/"])
class TestAPIResponse:
def test_example(self, api_client, base_url):
api_verbose_name = get_api_verbose_name(base_url=base_url)
...
... but this does not seem to be the ideal way for me.
Can I execute this set of tests without this helper function?
Going slightly off what #AnthonySottile said, if you know the endpoint you want to skip you can mark it in your call to pytest.mark.parametrize. The example below shows how to do that by taking advantage of pytest.param.
import pytest
def example(base, api):
return f"{base}{api}"
#pytest.mark.parametrize("base_url", ["api/v1/actions/", "api/v1/quotes/"])
class TestAPI:
#pytest.mark.parametrize("api_verbose_name",
["ação", pytest.param("declaração", marks=pytest.mark.skip)]
)
def test_example(self, base_url, api_verbose_name):
result = example(base_url, api_verbose_name)
assert result == f"{base_url}{api_verbose_name}"
And when we run the tests we can see that four tests are collected by only two are ran since the rest are skipped.
collected 4 items
test_foo.py ..ss [100%]
================================================================= 2 passed, 2 skipped in 0.02s ==================================================================
I have the following code.
How can I test the function create_items_by_parent_asin?
def get_amazon():
return AmazonAPI(settings.AMAZON_ACCESS_KEY, settings.AMAZON_SECRET_KEY, settings.AMAZON_ASSOC_TAG)
def get_item_by_asin(asin: str, response_group='Large'):
amazon = get_amazon()
product = amazon.lookup(ItemId=asin, ResponseGroup=response_group)
return product
def create_items_by_parent_asin(self, asin: str):
amazon_item = get_item_by_asin(asin, response_group='Large')
....
You don't test the API, you mock the interactions with amazon away with a different implementation of AmazonAPI.
In python this can be done using unittest.mock: https://docs.python.org/3/library/unittest.mock.html
It's been a long time since I've done this in python, but iirc you can just do something like this in your testclasses (untested, I adapted the example from the docs):
testproduct = ... # static product you will use in your tests
with patch('AmazonAPI') as mock:
instance = mock.return_value
instance.lookup.return_value = testproduct
product = x.create_items_by_parent_asin("...") # this should now be your testproduct
If product is a non-trivial thing to create an instance of you can also mock this away by doing:
testproduct = Mock()
testproduct.<method you want to mock>.return_value = ...
How can I create multiple TestCases and run them programmatically? I'm trying to test multiple implementations of a collection on a common TestCase.
I'd prefer to stick to with plain unittest and avoid dependencies.
Here's some resources that I looked at that didn't quite meet what I wanted:
Writing a re-usable parametrized unittest.TestCase method - The accepted answer proposes four different external libraries.
http://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases -
This approach uses a static method paramerize. I don't understand why you can't pass in a parameter directly into the
TestSubClass.__init__.
How to generate dynamic (parametrized) unit tests in python? - A little bit too black magic.
Here's a minimal (non)working example.
import unittest
MyCollection = set
AnotherCollection = set
# ... many more collections
def maximise(collection, array):
return 2
class TestSubClass(unittest.TestCase):
def __init__(self, collection_class):
unittest.TestCase.__init__(self)
self.collection_class = collection_class
self.maximise_fn = lambda array: maximise(collection_class, array)
def test_single(self):
self.assertEqual(self.maximise_fn([1]), 1)
def test_overflow(self):
self.assertEqual(self.maximise_fn([3]), 1)
# ... many more tests
def run_suite():
suite = unittest.defaultTestLoader
for collection in [MyCollection, AnotherCollection]:
suite.loadTestsFromTestCase(TestSubClass(collection))
unittest.TextTestRunner().run(suite)
def main():
run_suite()
if __name__ == '__main__':
main()
The above approach errors with in loadTestsFromTestCase:
TypeError: issubclass() arg 1 must be a class
How about using pytest with to parametrize fixture:
import pytest
MyCollection = set
AnotherCollection = set
def maximise(collection, array):
return 1
#pytest.fixture(scope='module', params=[MyCollection, AnotherCollection])
def maximise_fn(request):
return lambda array: maximise(request.param, array)
def test_single(maximise_fn):
assert maximise_fn([1]) == 1
def test_overflow(maximise_fn):
assert maximise_fn([3]) == 1
If that's not an option, you can make a mixin to contain test function, and subclasses to provide maximise_fns:
import unittest
MyCollection = set
AnotherCollection = set
def maximise(collection, array):
return 1
class TestCollectionMixin:
def test_single(self):
self.assertEqual(self.maximise_fn([1]), 1)
def test_overflow(self):
self.assertEqual(self.maximise_fn([3]), 1)
class TestMyCollection(TestCollectionMixin, unittest.TestCase):
maximise_fn = lambda self, array: maximise(MyCollection, array)
class TestAnotherCollection(TestCollectionMixin, unittest.TestCase):
maximise_fn = lambda self, array: maximise(AnotherCollection, array)
if __name__ == '__main__':
unittest.main()