Open philgyford opened 5 years ago
What sorts of problems have you run into so far? I haven't used FactoryBoy before, but from glancing at the pytest-factoryboy
docs I'd guess that you should be able to use both plugins at the same time. I wonder if the problems are FactoryBoy-related or MySQL-related.
If you have some non-MySQL tests that are failing, point them my way and I'll see if I can figure out what's up. Otherwise, I'll check on the progress in https://github.com/jeancochrane/pytest-flask-sqlalchemy/pull/10 and see if I can get it moving again.
Good point about MySQL... I'll try and find time to knock together a quick project using Postgres and see if I encounter the same issues.
This is a summary of an issue I was having with some similar code, and I believe it was the same thing when I switched to trying pytest-flask-sqlalchemy: https://stackoverflow.com/questions/54773484/saving-factoryboy-instances-to-database-with-flask-pytest-and-sqlalchemy But I should verify this again, and rule out MySQL difficulties!
Thanks for providing some sample code! I still don't have a full grasp on the integration between pytest and FactoryBoy, but this snippet in the SO link you shared looks intriguing:
from factory import Sequence
from factory.alchemy import SQLAlchemyModelFactory
from myapp.models import Company
from myapp.models.shared import db
class CompanyFactory(SQLAlchemyModelFactory):
name = Sequence(lambda n: "Company %d" % n)
class Meta:
model = Company
sqlalchemy_session = db.session
To attempt an integration with pytest-flask-sqlalchemy
, the first thing I would try here would be to use the configuration directives in pytest-flask-sqlalchemy
to mock out the myapp.models.shared.db.session
object that's being assigned to the sqlalchemy_session
above. You should be able to do that using the mocked-sessions
configuration directive in a your pytest config file, e.g.:
# setup.cfg
[tool:pytest]
mocked-sessions=myapp.models.shared.db.session
That should instruct pytest-flask-sqlalchemy
to replace all imports of myapp.models.shared.db.session
with a transactional db_session
object in application code whenever you require db_session
as a fixture in a test.
Let me know if that works! If not, the simplified Postgres example will be helpful for reproducing the problem and helping me hack around on it.
Thanks. I have now got pytest-flask-sqlalchemy working with my app that uses MySQL, although I'm not doing anything particularly advanced, database-wise. The mocked-sessions
pointer was crucial, thank you!
I tried again to get FactoryBoy working, but no joy. It seems it wasn't rolling the database back between tests -- I'd get errors about duplicates for model fields that should be unique from subsequent tests. I'll have to try putting together a minimal example, combining pytest-flask-sqlalchemy and FactoryBoy when I get a chance.
A reproducible example of your setup would help. We use FactoryBoy with pytest-flask-sqlalchemy + postgres without any troubles.
I'm not using this library either but I implemented the same pattern myself and so far the only way I got it working was by defining the factory as a pytest fixture too.
So I got my session
fixture defined, similar to the db_session
:
@pytest.fixture(scope="function")
def session(db):
....
yield session
....
My user factory:
@pytest.fixture
def user_factory(session):
"""Fixture to inject the session dependency."""
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
"""User model factory."""
class Meta:
model = User
exclude = ("id", "created_at")
sqlalchemy_session = session
sqlalchemy_session_persistence = "commit"
id = factory.Sequence(lambda n: n)
first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name")
email = factory.Faker("email")
password = factory.Faker("password", length=10)
return UserFactory
So in a test function you can use it like this:
def test_duplicate_email(session, user_factory):
"""Test persisting two users with same email."""
user_factory.create(email="duplicate@example.com")
session.begin_nested()
with pytest.raises(IntegrityError):
user_factory.create(email="duplicate@example.com")
session.rollback()
assert session.query(User).count() == 1
I've tried to make it work with the pytest-factoryboy
plugin but I simply couldn't. They kind of refuse to support it: issue, issue
@philgyford @killthekitten
I have the same issue, I investigated a little bit, and I guess the issue is that mocked-sessions patches session after it is bind to Factory, so in this example
from factory import Sequence
from factory.alchemy import SQLAlchemyModelFactory
from myapp.models import Company
from myapp.models.shared import db
class CompanyFactory(SQLAlchemyModelFactory):
name = Sequence(lambda n: "Company %d" % n)
class Meta:
model = Company
sqlalchemy_session = db.session # this holds original unmocked session
CompanyFactory._meta.sqalchemy_session
holds reference to origin db.session
.
I can only assume, that it happens, because db.session
has been patched after initialization of factory.
So @layoaster solution is only one, that suppose to work, but didn't work for me, I'm getting:
sqlalchemy.orm.exc.FlushError: Instance <Company at 0x10ddf8080> has a NULL identity key.
If this is an auto-generated value, check that the database table allows generation of new primary key values, and that the mapped Column object is configured to expect these generated values.
Ensure also that this flush() is not occurring at an inappropriate time, such as within a load() event.
Right, I use vanilla FactoryBoy, without the pytest-
prefix 👍 it might be a solution as well, but you'll obviously loose some handy helpers.
I also forgot to mention @citizen-stig that my sample code assumed to be using the vanilla FactoryBoy.
Anyways, at first sight, the exception doesn't seem to be related to the session object initialization but rather to the DB model object init.
Thanks for all this.. @layoaster's solution seems to work well!
The only difficulty I've found so far is how to use SubFactory
s. I tried doing this (see penultimate line):
import factory
@pytest.fixture
def author_factory(db_session):
class AuthorFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Author
sqlalchemy_session = db_session
sqlalchemy_session_persistence = "commit"
name = factory.Faker("name")
return AuthorFactory
@pytest.fixture
def book_factory(db_session):
class BookFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Book
sqlalchemy_session = db_session
sqlalchemy_session_persistence = "commit"
title = factory.Faker("sentence", nb_words=4)
author = factory.SubFactory(author_factory(db_session))
return BookFactory
But calling fixtures directly isn't allowed and gets this error:
Fixture "author_factory" called directly. Fixtures are not meant to be called directly, but are created automatically when test functions request them as parameters.
Maybe there isn't a way with this setup.
@philgyford when a fixture needs anothers fixture you just pass them via paremeters so your fixture book_factory should look like this:
@pytest.fixture
def book_factory(db_session, author_factory):
class BookFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Book
sqlalchemy_session = db_session
sqlalchemy_session_persistence = "commit"
title = factory.Faker("sentence", nb_words=4)
author = factory.SubFactory(author_factory)
return BookFactory
@layoaster Thank you! I swear, one day my brain will find pytest simple...
I'm doing something like this:
class BaseFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
sqlalchemy_session = scoped_session(
lambda: current_app.extensions["sqlalchemy"].db.session
)
Which works... almost... The first time you create() a factory object it works great. The next time you do it in the next test, you get a transient object. I think the FactoryBoy session is not getting refreshed for each test. Not sure how to address this.
If I try setting:
sqlalchemy_session_persistence = "commit"
I get the error:
../../../../.virtualenvs/hr-backend-PnsIdmCs/lib/python3.7/site-packages/sqlalchemy/engine/base.py:382: in connection
return self._revalidate_connection()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <sqlalchemy.engine.base.Connection object at 0x10e3c3828>
def _revalidate_connection(self):
if self.__branch_from:
return self.__branch_from._revalidate_connection()
if self.__can_reconnect and self.__invalid:
if self.__transaction is not None:
raise exc.InvalidRequestError(
"Can't reconnect until invalid "
"transaction is rolled back"
)
self.__connection = self.engine.raw_connection(_connection=self)
self.__invalid = False
return self.__connection
> raise exc.ResourceClosedError("This Connection is closed")
E sqlalchemy.exc.ResourceClosedError: This Connection is closed
Hello - I'm attempting to use this library with FactoryBoy, and afaict, I've set everything up correctly, but I'm still seeing commits persist beyond each test lifecycle. This is what I've got: pytest.ini
[pytest]
mocked-sessions=storage.connection.db.session
conftest.py
import logging
import os
import pathlib
from typing import TYPE_CHECKING, Type
import pytest
from app import create_app, Config
from .compat import * # noqa: F403,F401
if TYPE_CHECKING:
from models.users import User # noqa: F401
from pytests import factories
logging.captureWarnings(True)
logging.getLogger("faker").setLevel(logging.ERROR)
CUR_DIR = pathlib.Path(__file__).parent.resolve()
AUDIT = CUR_DIR / "audit.db"
DEFAULT = CUR_DIR / "default.db"
os.environ["DISABLE_TRACING"] = "0"
os.environ["DISABLE_JSON_LOGGING"] = "1"
@pytest.fixture(scope="session")
def database(request):
if AUDIT.exists():
AUDIT.unlink()
AUDIT.touch()
if DEFAULT.exists():
DEFAULT.unlink()
DEFAULT.touch()
@pytest.fixture(scope="session")
def app(database, request):
Config.SQLALCHEMY_BINDS.update(
default=f"sqlite:///{str(DEFAULT)}",
replica1=f"sqlite:///{str(DEFAULT)}",
audit=f"sqlite:///{str(AUDIT)}",
)
Config.SQLALCHEMY_DATABASE_URI = Config.SQLALCHEMY_BINDS["default"]
app = create_app()
app.testing = True
app.env = "testing"
with app.app_context():
yield app
@pytest.fixture(scope="session")
def client(app):
with app.test_client() as client:
yield client
@pytest.fixture(scope="session")
def _db(app):
from storage.connection import db
try:
db.create_all(bind=["default", "audit"])
except Exception:
db.session.rollback()
db.create_all(bind=["default", "audit"])
db.session.commit()
yield db
# Automatically enable transactions for all tests, without importing any extra fixtures.
@pytest.fixture(autouse=True)
def enable_transactional_tests(db_session):
pass
@pytest.fixture(scope="function")
def factories(db_session):
from pytests import factories
return factories
@pytest.fixture
def default_user(factories) -> "User":
return factories.DefaultUserFactory.create()
factories.py
import factory
from factory.alchemy import SQLAlchemyModelFactory
from flask import current_app
from flask_sqlalchemy import get_state
from sqlalchemy.orm import scoped_session
from models.users import User
Session = scoped_session(
lambda: get_state(current_app).db.session,
scopefunc=lambda: get_state(current_app).db.session,
)
class DefaultUserFactory(SQLAlchemyModelFactory):
class Meta:
model = User
sqlalchemy_session = Session
sqlalchemy_session_persistence = "commit"
email = factory.Faker("email")
password = factory.Faker("password")
first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name")
test_factories.py
import pytest
from models.users import User
from storage.connection import db
def test_session(db_session):
assert db.session is db_session
assert User.query.session is db_session()
@pytest.mark.parametrize(argnames="n", argvalues=range(10))
def test_default_user_factory(default_user: User, n):
assert default_user.id
assert User.query.count() == 1, User.query.all()
Now - test_session()
always passes, so I know that the patching is working, however only the first iteration of test_default_user_factory
passes, after which every previous iteration's user is persisted to the database. I'm also using a lazy getter for the factory Session object, and only importing the factories module within the scope of the db_session
in the fixtures.
I can't think of another reason why this would be failing, but I know it's likely something I'm doing. Any thoughts on how I might rectify this?
@seandstewart did you find a solution? I see it as a consequence of #23 and I kind of embraced it: my factories just start counting at 1 and automatically increases (id = factory.Sequence
). See https://github.com/DevStonks/selfsolver-backend/blob/c8dbdf89169d89a589f4dd2222cb9b8050926e86/tests/factories.py for more details.
this worked for me, thanks
It would be useful to see an example of how to use pytest-flask-sqlalchemy with FactoryBoy/pytest-factoryboy, if they can work together. For example, I'm not clear what would be used for factory classes'
sqlalchemy_session
field.(Background: I use something like pytest-flask-sqlalchemy in my tests, and would like to switch to pytest-flask-sqlalchemy when the MySQL PR is included, and would also like to use FactoryBoy, but have never managed to get the two things to work well together.)