How pytest knows that a function argument is fixture? - python

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.

Related

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

Passing pytest function arguments to a fixture

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.

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.

Using pytest to do setup and teardown

I have a handful of tests in my test module that need some common setup and teardown to run before and after the test. I don't need the setup and teardown to run for every function, just a handful of them. I've found I can kind of do this with fixtures
#pytest.fixture
def reset_env():
env = copy.deepcopy(os.environ)
yield None
os.environ = env
def test_that_does_some_env_manipulation(reset_env):
# do some tests
I don't actually need to return anything from the fixture to use in the test function, though, so I really don't need the argument. I'm only using it to trigger setup and teardown.
Is there a way to specify that a test function uses a setup/teardown fixture without needing the fixture argument? Maybe a decorator to say that a test function uses a certain fixture?
Thanks to hoefling's comment above
#pytest.mark.usefixtures('reset_env')
def test_that_does_some_env_manipulation():
# do some tests
You could use autouse=True in your fixture. Autouse automatically executes the fixture at the beginning of fixture scope.
In your code:
#pytest.fixture(autouse=True)
def reset_env():
env = copy.deepcopy(os.environ)
yield None
os.environ = env
def test_that_does_some_env_manipulation():
# do some tests
But you need to be careful about the scope of the fixture as the fixture would be triggered for each scope. If you have all such tests under one directory, you can have it in a conftest file of the directory. Otherwise, you can declare the fixture in the test file.
Relevant pytest help doc

Categories

Resources