Pytest session scoped fixture yields multiple times - help me understand - python

Below is an example of a pytest session and function scoped fixtures.
The first fixture sets up a single db connection, whilst the second one deletes the DB state to have a clean slate for each test function.
My understanding of the yield in the session scoped fixture is wonky. I thought yield gives control over to testing functions once and then continues with tear down. But if we mix up function scoped fixture that depends on the session scope fixture, it keeps re-using the same instance of the db, implying yield is yielding control multiple times (however does not create the db multiple times). This is confusing to me. Can you help me out here with understanding this?
#pytest.fixture(scope="session")
def db():
"""CardsDB object connected to a temporary database"""
with TemporaryDirectory() as db_dir:
db_path = Path(db_dir)
db_ = cards.CardsDB(db_path)
yield db_
db_.close()
#pytest.fixture(scope="function")
def cards_db(db):
"""CardsDB object that's empty"""
db.delete_all()
return db

Related

How is sqlalchemy.orm.scoping.scoped_session proxying ability implemented?

I do understand, that a sqlalchemy.orm.scoping.scoped_session uses a session_factory to create a session and also possesses a registry to return an already present session through the __call__() call.
But one can also directly call the .query method upon scoped_session and that completely confuses me, since scoped_session:
1. does not have this method
2. is not a dynamic wrapper of a sqlalchemy.orm.session.Session and
3. is not a subclass of sqlalchemy.orm.session.Session.
How is scoped_session able to dispatch a query? I just don't see any indirection or abstraction that would allow for this.. yet it works.
from sqlalchemy.orm import sessionmaker,scoped_session
from sqlalchemy import create_engine
user, password, server, dbname = "123","123","123", "123"
s = 'oracle://%s:%s#%s/%s' % (user, password, server, dbname)
some_engine = create_engine(s)
_sessionmaker = sessionmaker(bind=some_engine)
sc_sess = scoped_session(_sessionmaker) # here sc_sess is an isntance of "sqlalchemy.orm.scoping.scoped_session"
sc_sess.query(...) # works! but why?
# the following is what i expect to work and to be normal workflow
session = sc_sess() # returns an instance of sqlalchemy.orm.session.Session
session.query(...)
This behaviour is described in the SqlAlchemy Documentation:
Implicit Method Access
The job of the scoped_session is simple; hold onto a Session for all who ask for it. As a means of producing more transparent access to this Session, the scoped_session also includes proxy behavior, meaning that the registry itself can be treated just like a Session directly; when methods are called on this object, they are proxied to the underlying Session being maintained by the registry:
Session = scoped_session(some_factory)
# equivalent to:
#
# session = Session()
# print(session.query(MyClass).all())
#
print(Session.query(MyClass).all())
The above code accomplishes the same task as that of acquiring the current Session by calling upon the registry, then using that Session.
So this behaviour is normal, but how is it implemented? (not proxy in general, but precisely in this example)
Thanks.
You've obviously had a good look at the sqlalchemy.orm.scoping.scoped_session class, and if you look just a little further down in the same module, you'll find the following snippet (link):
def instrument(name):
def do(self, *args, **kwargs):
return getattr(self.registry(), name)(*args, **kwargs)
return do
for meth in Session.public_methods:
setattr(scoped_session, meth, instrument(meth))
If we dissect that from the bottom up, we've first got the for meth in Session.public_methods: loop, where Session.public_methods is simply a tuple of the names of methods that a Session exposes, and the string "query" is one of those:
class Session(_SessionClassMethods):
...
public_methods = (
...,
"query",
...,
)
Each of those names (meth) in Session.public_methods is passed to the setattr call inside the loop:
setattr(scoped_session, meth, instrument(meth))
The value that is assigned to the name of the method on the scoped_session is the return value of the call to instrument(meth), which is a closure called, do(). That function calls the scoped_session.registry to get the registered Session object, gets the named method (meth), and calls it with the *args & **kwargs that were passed to do().
As the for meth in Session.public_methods: loop is defined in the global namespace of the module, it is executed at compile time, before anything else has a chance to use the scoped_session. So by the time your code can get a hold of a scoped_session instance, those methods have already been monkey patched on there.

What are the functional differences in these 3 pytest fixtures?

I'm relatively new to pytest-style unit testing, and I'm trying to learn more about pytest fixtures. I'm not passing a scope argument to the fixture, so I know that the scope is "function". Are there any functional differences in these 3 styles of simple fixtures? Why would one approach be favored over the others?
#pytest.fixture()
#patch('a.b.c.structlog.get_logger')
def fixture_classQ(mock_log):
handler = MagicMock(spec=WidgetHandler)
return ClassQ(handler)
#pytest.fixture()
def fixture_classQ():
with patch('a.b.c.structlog.get_logger'):
handler = MagicMock(spec=WidgetHandler)
return ClassQ(handler)
#pytest.yield_fixture()
def fixture_classQ():
with patch('a.b.c.structlog.get_logger'):
handler = MagicMock(spec=WidgetHandler)
yield ClassQ(handler)
Simple example usage of the fixture:
def test_classQ_str(fixture_classQ):
assert str(fixture_classQ) == "This is ClassQ"
Thanks.
fixture 1
Starting with the first one, this creates a plain-data fixture. The mock is (imo misleadingly) only alive for the duration of the fixture function because it uses return.
In order ~roughly what happens for that:
pytest notices your fixture is used for the test function
it calls the fixture function
the mock decorator starts the patch
the mock decorator calls your actual function (which returns a value)
the mock decorator undoes the patch
pytest notices it wasn't a generator and so that's the value of your fixture
fixture 2
the second is identical in behaviour to the first, except it uses the context manager form of mock instead of the decorator. personally I don't like the decorator form but that's just me :D
fixture 3
(first before I continue, pytest.yield_fixture is a deprecated alias for pytest.fixture -- you can just use #pytest.fixture)
The third does something different! The patch is alive for the duration of the test because it has "yielded" during the fixture. This is a kind of way to create a setup + teardown fixture all in one. Here's roughly the execution here
pytest notices your fixture is used for the test function
pytest calls the fixture function
since it is a generator, it returns immediately without executing code
pytest notices it is a generator, calls next(...) on it
this causes the code to execute until the yield and then "pausing". you can think of it kind of as a co-routine
the __enter__ of the mock is called making the patch active
the value that is yielded is used as the value of the fixture
pytest then executes your test function
pytest then calls next(...) again on the generator to exhaust the fixture
this __exit__s the with statement, undoing the patch
which to choose?
the best answer is it depends. Since 1 and 2 are functionally equivalent it's up to personal preference. Pick 3. if you need the patch to be active during the entire duration of your test. And don't use pytest.yield_fixture, just use pytest.fixture.

How does pytest deal with fixtures calling other fixtures?

I have two pytest fixtures, client and app. client calls app.
The test function test_register has arguments client and app and hence calls both fixtures.
My question is if the instance of app used in test_register is always going to be the one that client called, and if this is how pytest works in general (the assertion in test_register passes, so it is true in this case) .
In other words, does pytest generate unrelated instances for each argument in a test function that calls a fixture or does it call the fixtures and the instances returned also reference each other?
Here's the code:
#pytest.fixture
def app():
app = create_app({
'TESTING': True,
})
yield app
#pytest.fixture
def client(app):
return app.test_client()
def test_register(client, app):
assert client.application is app
All fixtures have a scope, the implicit scope being function but there's also class, module and session scopes. Within each scope there will only ever be one instance created of a fixture.
So in your example both app and client are using the function-scope. When executing test_register it enters the function-scope of this test and creates the fixture intances. Hence both test_register and client get the same instance of app.
See the docs for more details on how this all works.

py.test: Temporary folder for the session scope

The tmpdir fixture in py.test uses the function scope and thus isn't available in a fixture with a broader scope such as session. However, this would be useful for some cases such as setting up a temporary PostgreSQL server (which of course shouldn't be recreated for each test).
Is there any clean way to get a temporary folder for a broader scope that does not involve writing my own fixture and accessing internal APIs of py.test?
Since pytest release 2.8 and above the session-scoped tmpdir_factory fixture is available. See the example below from the documentation.
# contents of conftest.py
import pytest
#pytest.fixture(scope='session')
def image_file(tmpdir_factory):
img = compute_expensive_image()
fn = tmpdir_factory.mktemp('data').join('img.png')
img.save(str(fn))
return fn
# contents of test_image.py
def test_histogram(image_file):
img = load_image(image_file)
# compute and test histogram
Unfortunately there is currently no way (2014) of doing this nicely. In the future py.test will introduce a new "any" scope or something similar for this, but that's the future.
Right now you have to do this manually yourself. However as you note you lose quite a few nice features: symlinks in /tmp to the last test, auto cleanup after a few test runs, sensibly named directories etc. If the directory is not too expensive I usually combine a session and function scoped fixture in the following way:
#pytest.fixture(scope='session')
def session_dir(request):
temp_dir = py.path.local(tempfile.mkdtemp())
request.addfinalizer(lambda: folder.remove(rec=1))
# Any extra setup here
return temp_dir
#pytest.fixture
def temp_dir(session_dir, tmpdir):
session_dir.copy(tmpdir)
return tmpdir
This creates a temporary directory which gets cleaned up after a test run, however for each test which actually needs it (by requesting temp_dir) gets a copy which is saved with the tmpdir semantics.
If tests actually need to share state via this directory then the finalizer of temp_dir would have to copy things back to the session_dir. This is however not a very good idea since it makes the tests reliant on the execution order and would also cause problems when using pytest-xdist.
I add a finalizer when I want to delete all temporary folders created in session.
_tmp_factory = None
#pytest.fixture(scope="session")
def tmp_factory(request, tmpdir_factory):
global _tmp_factory
if _tmp_factory is None:
_tmp_factory = tmpdir_factory
request.addfinalizer(cleanup)
return _tmp_factory
def cleanup():
root = _tmp_factory.getbasetemp().strpath
print "Cleaning all temporary folders from %s" % root
shutil.rmtree(root)
def test_deleting_temp(tmp_factory):
root_a = tmp_factory.mktemp('A')
root_a.join('foo.txt').write('hello world A')
root_b = tmp_factory.mktemp('B')
root_b.join('bar.txt').write('hello world B')
for root, _, files in os.walk(tmp_factory.getbasetemp().strpath):
for name in files:
print(os.path.join(root, name))
The output should be like:
/tmp/pytest-of-agp/pytest-0/.lock
/tmp/pytest-of-agp/pytest-0/A0/foo.txt
/tmp/pytest-of-agp/pytest-0/B0/bar.txt
Cleaning all temporary folders from /tmp/pytest-of-agp/pytest-0
Here's another approach. It looks like pytest doesn't remove temporary directories after test runs. The following is a regular function-scoped fixture.
# conftest.py
TMPDIRS = list()
#pytest.fixture
def tmpdir_session(tmpdir):
"""A tmpdir fixture for the session scope. Persists throughout the session."""
if not TMPDIRS:
TMPDIRS.append(tmpdir)
return TMPDIRS[0]
And to have persistent temporary directories across modules instead of the whole pytest session:
# conftest.py
TMPDIRS = dict()
#pytest.fixture
def tmpdir_module(request, tmpdir):
"""A tmpdir fixture for the module scope. Persists throughout the module."""
return TMPDIRS.setdefault(request.module.__name__, tmpdir)
Edit:
Here's another solution that doesn't involve global variables. pytest 1.8.0 introduced a tmpdir_factory fixture that we can use:
#pytest.fixture(scope='module')
def tmpdir_module(request, tmpdir_factory):
"""A tmpdir fixture for the module scope. Persists throughout the module."""
return tmpdir_factory.mktemp(request.module.__name__)
#pytest.fixture(scope='session')
def tmpdir_session(request, tmpdir_factory):
"""A tmpdir fixture for the session scope. Persists throughout the pytest session."""
return tmpdir_factory.mktemp(request.session.name)

pytest fixture with scope session running for every test

Correct me if I'm wrong, but if a fixture is defined with scope="session", shouldn't it be run only once per the whole pytest run?
For example:
import pytest
#pytest.fixture
def foo(scope="session"):
print('foooooo')
def test_foo(foo):
assert False
def test_bar(foo):
assert False
I have some tests that rely on data retrieved from some APIs, and instead of querying the API in each test, I rather have a fixture that gets all the data at once, and then each test uses the data it needs. However, I was noticing that for every test, a request was made to the API.
That's because you're declaring the fixture wrong. scope should go into the pytest.fixture decoraror parameters:
#pytest.fixture(scope="session")
def foo():
print('foooooo')
In your code, the scope is left to default value function, that's why the fixture is being ran for each test.

Categories