pytest fixture with argument - python

It seems it it possible to pass argument to fixtures:
Pass a parameter to a fixture function
yet, when implementing this minimal example, I get an error.
import pytest
#pytest.fixture
def my_fixture(v):
print("fixture in")
yield v+1
print("fixture out")
#pytest.mark.parametrize("my_fixture",[1], indirect=True)
def test_myfixture(my_fixture):
print(my_fixture)
#pytest.fixture
def my_fixture(v):
E fixture 'v' not found
Is anything wrong with the code above ?
(python 3.8.10, pytest-6.2.5)

To briefly elaborate on Vince's answer: in general, arguments to fixture functions are interpreted as the names of other fixtures to load before the one being defined. That's why you got the error message fixture 'v' not found: pytest thought that you wanted my_fixture to depend on another fixture called v that doesn't exist.
The request argument is an exception to this general rule. It doesn't refer to another fixture. Instead, it instructs pytest to give the fixture access to the request object, which contains information on the currently running test, e.g. which parameters are being tested (via request.param), which markers the test has (via request.node.get_closest_marker()), etc.
So to make use of indirect parametrization, your fixture needs to (i) accept the request argument and (ii) do something with request.param. For more information, here are the relevant pages in the documentation:
The request argument
Parametrized fixtures
Indirect parametrization

This works:
#pytest.fixture
def my_fixture(request):
print("fixture in")
yield request.param+1 # difference here !
print("fixture out")
#pytest.mark.parametrize("my_fixture",[1], indirect=True)
def test_myfixture(my_fixture):
print(my_fixture)

There is nothing wrong. Fixtures are used to fix constants to reuse them identically in multiple tests. They don't accept any argument. By the way you can create a pytest.fixture that is a "constant" function:
#pytest.fixture
def my_fixture():
return lambda x: x+1
And avoid print in fixture (on the final version at least).

Related

pytest.parametrize get pytest.fixture function instead of the actual object [duplicate]

I would like to use fixtures as arguments of pytest.mark.parametrize or something that would have the same results.
For example:
import pytest
import my_package
#pytest.fixture
def dir1_fixture():
return '/dir1'
#pytest.fixture
def dir2_fixture():
return '/dir2'
#pytest.parametrize('dirname, expected', [(dir1_fixture, 'expected1'), (dir2_fixture, 'expected2')])
def test_directory_command(dirname, expected):
result = my_package.directory_command(dirname)
assert result == expected
The problem with fixture params is that every param of the fixture will get run every time it's used, but I don't want that. I want to be able to choose which fixtures will get used depending on the test.
Will was on the right path, you should use request.getfixturevalue to retrieve the fixture.
But you can do it right in the test, which is simpler.
#pytest.mark.parametrize('dirname, expected', [
('dir1_fixture', 'expected1'),
('dir2_fixture', 'expected2')])
def test_directory_command(dirname, expected, request):
result = my_package.directory_command(request.getfixturevalue(dirname))
assert result == expected
Another way is to use lazy-fixture plugin:
#pytest.mark.parametrize('dirname, expected', [
(pytest.lazy_fixture('dir1_fixture'), 'expected1'),
(pytest.lazy_fixture('dir2_fixture'), 'expected2')])
def test_directory_command(dirname, expected):
result = my_package.directory_command(dirname)
assert result == expected
If you're on pytest 3.0 or later, I think you should be able to solve this particular scenario by writing a fixture using getfixturevalue:
#pytest.fixture(params=['dir1_fixture', 'dir2_fixture'])
def dirname(request):
return request.getfixturevalue(request.param)
However, you can't use this approach if the fixture you're attempting to dynamically load is parametrized.
Alternatively, you might be able to figure something out with the pytest_generate_tests hook. I haven't been able to bring myself to look into that much, though.
This isn't currently supported by pytest. There is an open feature request for it though (which has been opened in 2013).
As for now, my only solution is to create a fixture that returns a dictionary of fixtures.
import pytest
import my_package
#pytest.fixture
def dir1_fixture():
return '/dir1'
#pytest.fixture
def dir2_fixture():
return '/dir2'
#pytest.fixture
def dir_fixtures(
dir1_fixture,
dir2_fixture
):
return {
'dir1_fixture': dir1_fixture,
'dir2_fixture': dir2_fixture
}
#pytest.mark.parametrize('fixture_name, expected', [('dir1_fixture', 'expected1'), ('dir2_fixture', 'expected2')])
def test_directory_command(dir_fixtures, fixture_name, expected):
dirname = dir_fixtures[fixture_name]
result = my_package.directory_command(dirname)
assert result == expected
Not the best since it does not use a solution built into pytest, but it works for me.
DO NOT TRY TO CHANGE FIXTURE PARAMETERS DURING TEST EXECUTION
Invalid example: #pytest.fixture(scope="class", params=other_fixture)
Now I'll explain why it doesn't work:
Pytest creates session objects before running the test, containing the parameters with which the test will run. During the execution of the test; you cannot change the parameters
If you really want to do this (change the parameters dynamically), you can use an intermediate text file: "params.txt".
Example: #pytest.fixture(scope="class", params=json.load(open("topics.txt"))).
Again, you will not be able to change the content of the file during the test; because if you change it; will not be visible in the test. To do this; we need to change the contents of the file when the program starts and before the session objects are created. To do that; define a method pytest_sessionstart(session) in conftest.py where you change the file content.
For more details; check this documentation: How to run a method before all tests in all classes? and https://docs.pytest.org/en/6.2.x/reference.html#pytest.hookspec.pytest_sessionstart

How to unit test a function when it is calling a member function of one of the argument

I want to unit test the following function in Python:
def get_params(env, secret_fetcher):
try:
url = env['API_URL']
except KeyError:
raise
try:
key = secret_fetcher.get_secret('CLIENT-KEY')
secret = secret_fetcher.get_secret('CLIENT-SECRET')
except:
raise
return url, key, secret
It read one parameter from environment while retrieve the other two from a key vault using an object of KeyVault class secret_fetcher. I call it in my main function like below:
secret_fetcher = SecretFetcher(vault_url)
url, key, secret = get_params(os.environ, secret_fetcher)
I am writing a unit test for this function. For env I am using a dictionary inside the test. However what do I do with second argument of the function whose member function is being called inside the function to test?
class TestApp():
def test_get_params(self):
env = {'WrongField': 'http://test.com/123'}
<mock secret_fetcher>
self.assertRaises(KeyError, get_params, env, secret)
Do I mock secret_fetcher or secret_fetcher.get_secret? Especially when get_secret returns different value when fed with different argument of its own. Should I mock the class SecretFetcher and implement a function get_secret that returns expected output for argument with these two different values?
If you are only intending to test the exception as-is, mocking the secret_fetcher argument is basically inconsequential at this stage, as a simple None value will do as it will never be touched, but here's an example to kick things off:
# include the `get_param` function by import or inline here
import unittest
from unittest.mock import Mock
class TestApp(unittest.TestCase):
def test_get_params_missing_url(self):
env = {'missing': 'fail'}
secret_fetcher = Mock()
with self.assertRaises(KeyError):
get_params(env, secret_fetcher)
(Do note that I prefer using assertRaises as a context manager to ensure a more natural way of writing the calling of a function; do note that the first exception in the with block will prevent subsequent code from being executed in that block, so it's recommended that only one logical expression be in the assertRaises context manager, or at the very least be the last line; i.e. this can only test one exception at a time)
Running this one test:
$ python -m unittest demo.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
However, given that the theory behind using mocks in the context of unit testing is to enable the testing of the code using the bare minimum external dependencies (i.e. using no other real modules, methods or classes; this keep the testing done against just the relevant unit), using the other features provided by unittest.mock.Mock and friends may simplify this goal.
You may wish to ensure that get_secret was called with the correct argument and the expected result was to be returned, should the correct env was provided. Also testing that the error handling was dealt with as expected. Additional methods that may be appended to the TestApp class above:
def test_get_params_success(self):
env = {'API_URL': 'https://api.example.com'}
def get_secret(arg):
return arg
secret_fetcher = Mock()
secret_fetcher.get_secret.side_effect = get_secret
url, key, secret = get_params(env, secret_fetcher)
self.assertEqual(url, 'https://api.example.com')
self.assertEqual(key, 'CLIENT-KEY')
self.assertEqual(secret, 'CLIENT-SECRET')
# Test that the secret_fetcher.get_secret helper was called
# with both arguments
secret_fetcher.get_secret.assert_any_call('CLIENT-KEY')
secret_fetcher.get_secret.assert_any_call('CLIENT-SECRET')
self.assertEqual(
secret_fetcher.get_secret.call_args[0], ('CLIENT-SECRET',))
def test_get_params_failure(self):
env = {'API_URL': 'https://api.example.com'}
secret_fetcher = Mock()
secret_fetcher.get_secret.side_effect = ValueError('wrong value')
with self.assertRaises(ValueError):
get_params(env, secret_fetcher)
# Test secret_fetcher.get_secret helper was only called with
# the first CLIENT-KEY argument
# Python 3.8 can check secret_fetcher.get_secret.call_args.args
self.assertEqual(
secret_fetcher.get_secret.call_args[0], ('CLIENT-KEY',))
Testing it out:
$ python -m unittest demo.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Note that while I have absolute zero information about what your SecretFetcher class does or has, the three test cases together tested the provided get_params function to ensure that it behaves as expected, including testing how it should have used the secret_fetcher.get_secret, that it handles the error as expected, and that with all the provided test cases tested every line of code in the get_params example provided in the question.
Hopefully this served as a comprehensive example on how mocks might be used to satisfy the goals of unit testing.

How to execute a parameterized fixture for only some of the parameters?

From the official documentation, in the example about parametrizing fixtures:
Parametrizing fixtures
Extending the previous example, we can flag the fixture to create two smtp_connection fixture instances which will cause all tests using the fixture to run twice.
#pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
I wrote a parametrized fixture like the above example, but now my problem is I want to use the fixture in a different test function that should only execute once for one of the parameters...Like so:
def my_test_function(smtp_connection)
# I want this function to execute only once for the first or the second parameter...
So my question is: Can a test function use the fixture and be executed only for some parameters using pytest API? Or is this use case already a mistake and should both the fixture or the test functions be implemented differently in such case? If so, what would conceptually be the correct design alternative?
I am looking for a programmatic solution that doesn't require using command-line flags when I run pytest.
You could use indirect fixture parametrization - in this case you would define the parameters you want to use in the test instead of the fixture:
#pytest.fixture(scope="module")
def smtp_connection(request):
url = request.param
...
pytest.mark.parametrize("smtp_connection", ["smtp.gmail.com", "mail.python.org"], indirect=True)
def def my_test_function(smtp_connection):
...
pytest.mark.parametrize("smtp_connection", ["smtp.gmail.com"], indirect=True)
def def my_other_test_function(smtp_connection):
...
This will parametrize the fixture with each of the parameters you provide in the list for a specific test. You can read the parameter from request.param as shown above.
Of course, if you have many tests that use the same parameters you are probably better off using specific fixtures instead.

Module level fixture is not running

I want to have a specific setup/tear down fixture for one of the test modules. Obviously, I want it to run the setup code once before all the tests in the module, and once after all tests are done.
So, I've came up with this:
import pytest
#pytest.fixture(scope="module")
def setup_and_teardown():
print("Start")
yield
print("End")
def test_checking():
print("Checking")
assert True
This does not work that way. It will only work if I provide setup_and_teardown as an argument to the first test in the module.
Is this the way it's supposed to work? Isn't it supposed to be run automatically if I mark it as a module level fixture?
Module-scoped fixtures behave the same as fixtures of any other scope - they are only used if they are explicitely passed in a test, marked using #pytest.mark.usefixtures, or have autouse=True set:
#pytest.fixture(scope="module", autouse=True)
def setup_and_teardown():
print("setup")
yield
print("teardown")
For module- and session-scoped fixtures that do the setup/teardown as in your example, this is the most commonly used option.
For fixtures that yield an object (for example an expansive resource that shall only be allocated once) that is accessed in the test, this does not make sense, because the fixture has to be passed to the test to be accessible. Also, it may not be needed in all tests.

How pytest knows that a function argument is fixture?

According to pytest Documentation:
fixtures are implemented in a modular manner, as each fixture name triggers a fixture function which can itself use other fixtures.
import pytest
#pytest.yield_fixture
def passwd():
with open("/etc/passwd") as f:
yield f.readlines()
def test_has_lines(passwd):
assert len(passwd) >= 1
For the above code, function test_has_lines has an input argument "passwd" and pytest will load it from generated value of passwd().
How pytest knows that an argument (in this case passwd) is fixture?
I am more interested in the internal working of it.

Categories

Resources