Passing pytest function arguments to a fixture - python

I am trying to create a fixure that simply prints the arguments of a pytest test case.
For example:
#pytest.fixture(scope='function')
def print_test_function_arguments(request):
# Get the value of argument_1 from the run of the test function
print(f'argument_1 = {value_1}')
def test_something(print_test_function_arguments, argument_1, argument_2):
assert False

If you want to do any kind of introspection, request fixture is the way to go. request.node gives you the current test item, request.node.function the test_something function object and request.getfixturevalue("spam") will evaluate the fixture spam and return its result (or take it from fixture cache if already evaluated before). A simple args introspection example (untested):
import inspect
import pytest
#pytest.fixture(scope='function')
def print_test_function_arguments(request):
argspec = inspect.getfullargspec(request.node.function)
positional_args = argspec.args
positional_args.remove("print_test_function_arguments")
for argname in positional_args:
print(argname, "=", request.getfixturevalue(argname))
Of course, you can't evaluate the fixture print_test_function_arguments in its body, otherwise it will stuck in an infinite recursion, so its name must be removed from arguments list first.

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

pytest fixture with argument

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).

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 do I have to mock patch in this use case?

The initial scenerio is writing tests for functions from a library (lib.py).
lib.py:
def fun_x(val):
# does something with val
return result
def fun(val):
x = fun_x(val)
# does seomthing with x
return result
test__lib.py
import pytest
import lib
def lib_fun_x_mocked(val):
return "qrs"
def test_fun():
assert lib.fun("abc") == "xyz"
But lib.fun_x() does something very expensive or requires a resource not reliably available or not determinisitc. So I want to subsitute it with a mock function such that when the test test_fun() is executed lib.fun() uses lib_fun_x_mocked() instead of fun_x() from its local scope.
So far I'm running into cryptic error messages when I try to apply mock/patch recipes.
You can use the built-in fixture monkeypatch provided by pytest.
import lib
def lib_fun_x_mocked(some_val): # still takes an argument
return "qrs"
def test_fun(monkeypatch):
with monkeypatch.context() as mc:
mc.setattr(lib, 'fun_x', lib_fun_x_mocked)
result = lib.fun('abc')
assert result == 'qrs'
Also as a side note, if you are testing the function fun you shouldn't be asserting the output of fun_x within that test. You should be asserting that fun behaves in the way that you expect given a certain value is returned by fun_x.

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