Pytest and Django settings runtime changes - python

I have a receiver that needs to know whether DEBUG set to True in my settings.py.
from django.conf import settings
...
#receiver(post_save, sender=User)
def create_fake_firebaseUID(sender, instance, created=False, **kwargs):
# Fake firebaseUID if in DEBUG mode for development purposes
if created and settings.DEBUG:
try:
instance.userprofile
except ObjectDoesNotExist:
UserProfile.objects.create(user=instance, firebaseUID=str(uuid.uuid4()))
The problem is that when I create a user using manage.py shell everything works as expected. However, if I run my tests via py.test, the value of settings.DEBUG changes to False. If I check it in conftest.py in pytest_configure, DEBUG is set to True. It changes somewhere later and I have no idea where.
What can cause this? I am sure that I do not change it anywhere in my code.
Edit.
conftest.py
import uuid
import pytest
import tempfile
from django.conf import settings
from django.contrib.auth.models import User
#pytest.fixture(scope='session', autouse=True)
def set_media_temp_folder():
with tempfile.TemporaryDirectory() as temp_dir:
settings.MEDIA_ROOT = temp_dir
yield None
def create_normal_user() -> User:
username = str(uuid.uuid4())[:30]
user = User.objects.create(username=username)
user.set_password('12345')
user.save()
return user
#pytest.fixture
def normal_user() -> User:
return create_normal_user()
#pytest.fixture
def normal_user2() -> User:
return create_normal_user()
myapp/tests/conftest.py
# encoding: utf-8
import os
import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from userprofile.models import ProfilePicture
#pytest.fixture
def test_image() -> bytes:
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(DIR_PATH, 'test_image.jpg'), 'rb') as f:
yield f
#pytest.fixture
def profile_picture(test_image, normal_user) -> ProfilePicture:
picture = SimpleUploadedFile(name='test_image.jpg',
content=test_image.read(),
content_type='image/png')
profile_picture = ProfilePicture.objects.get(userprofile__user=normal_user)
profile_picture.picture = picture
profile_picture.save()
return profile_picture
pytest.ini
[pytest]
addopts = --reuse-db
DJANGO_SETTINGS_MODULE=mysite.settings

Apparently pytest-django explicitly sets DEBUG to False (source code link).
Diving through the git history of pytest-django a bit, this was done to match Django's default behavior (pytest commit link).
From the Django docs:
Regardless of the value of the DEBUG setting in your configuration file, all Django tests run with DEBUG=False. This is to ensure that
the observed output of your code matches what will be seen in a
production setting.
As a workaround you can use pytest-django's settings fixture to override so DEBUG=True if you need it to be. For example,
def test_my_thing(settings):
settings.DEBUG = True
# ... do your test ...

For anyone who is having similar problem. I found the reason. I downloaded source files of pytest-django and found out that it sets DEBUG to False in pytest-django/pytest_django/plugin.py:338. I do not know why tho.

Add the following line in the pytest.ini file:
django_debug_mode = True

Related

Django - Turn settings.DEBUG to True for testing Swagger endpint

I have this test for checking if I can ping the swagger endpoint
from django.test import SimpleTestCase
from django.test.utils import override_settings
from django.urls import reverse
from rest_framework import status
class SwaggerTest(SimpleTestCase):
#override_settings(DEBUG=True)
def test_successful_swagger_ping(self):
"""
Test to ensure that Swagger can be reached successfully. If this test
throws 5XX that means there's an error in swagger annotation on some view function.
"""
response = self.client.get(reverse('swagger'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
So this test fails, because I only have swagger added to url when settings.DEBUG=True. I thought #override_settings(DEBUG=TRUE) would fix that, but it doesn't. My test passes when I manually set settings.DEBUG=True under my settings.py file, otherwise it throws an error because it can't find the endpoint. I've tried decorating both the class and the function with #override_settings and in both cases it threw the error because it couldn't find the endpoint. I'm not particularly attached to this way of testing to see if Swagger is working. This test is just to check if Swagger isn't returning an error regarding annotation. If anyone knows a better way to test this I'm open to that.
I've also tried
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
class SwaggerTest(TestCase):
#staticmethod
def setUpClass() -> None:
settings.DEBUG = True
super(SwaggerTest, SwaggerTest).setUpClass()
def test_successful_swagger_ping(self):
"""
Test to ensure that Swagger can be reached successfully. If this test
throws 5XX that means there's an error in swagger annotation on some view function.
"""
response = self.client.get(reverse('swagger'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
but that also gives django.urls.exceptions.NoReverseMatch: Reverse for 'swagger' not found. 'swagger' is not a valid view function or pattern name. it only works when I set
test/__init__.py
settings.DEBUG = True
Is it ok to change settings?
Short answer: No, unless you do it during the startup.
Long answer: You should not modify settings at runtime. This means, no settings modifications after the app has been started, like changing configuration in views.py, serializers.py, models.py or other modules you add during the development. But it is OK to modify settings if they depend on local variables if you do it at startup and you are fully aware of what happens.
But this is how I usually override settings in tests:
from django.conf import settings
from django.test import TestCase
class MyTestCase(TestCase): # noqa
#staticmethod
def setUpClass(): # noqa
settings.DEBUG = True
super(ExampleTestCase, ExampleTestCase).setUpClass()
But maybe you should run your tests twice for that?

Test Pydantic settings in FastAPI

Suppose my main.py is like this (this is a simplified example, in my app I use an actual database and I have two different database URIs for development and testing):
from fastapi import FastAPI
from pydantic import BaseSettings
app = FastAPI()
class Settings(BaseSettings):
ENVIRONMENT: str
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()
databases = {
"dev": "Development",
"test": "Testing"
}
database = databases[settings.ENVIRONMENT]
#app.get("/")
def read_root():
return {"Environment": database}
while the .env is
ENVIRONMENT=dev
Suppose I want to test my code and I want to set ENVIRONMENT=test to use a testing database. What should I do? In FastAPI documentation (https://fastapi.tiangolo.com/advanced/settings/#settings-and-testing) there is a good example but it is about dependencies, so it is a different case as far as I know.
My idea was the following (test.py):
import pytest
from fastapi.testclient import TestClient
from main import app
#pytest.fixture(scope="session", autouse=True)
def test_config(monkeypatch):
monkeypatch.setenv("ENVIRONMENT", "test")
#pytest.fixture(scope="session")
def client():
return TestClient(app)
def test_root(client):
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"Environment": "Testing"}
but it doesn't work.
Furthermore I get this error:
ScopeMismatch: You tried to access the 'function' scoped fixture 'monkeypatch' with a 'session' scoped request object, involved factories
test.py:7: def test_config(monkeypatch)
env\lib\site-packages\_pytest\monkeypatch.py:16: def monkeypatch()
while from pytest official documentation it should work (https://docs.pytest.org/en/3.0.1/monkeypatch.html#example-setting-an-environment-variable-for-the-test-session). I have the latest version of pytest installed.
I tried to use specific test environment variables because of this: https://pydantic-docs.helpmanual.io/usage/settings/#field-value-priority.
To be honest I'm lost, my only real aim is to have a different test configuration (in the same way Flask works: https://flask.palletsprojects.com/en/1.1.x/tutorial/tests/#setup-and-fixtures). Am I approaching the problem the wrong way?
PydanticSettings are mutable, so you can simply override them in your test.py:
from main import settings
settings.ENVIRONMENT = 'test'
This is a simple way that works for me. Consider that you have a configuration file named APPNAME.cfg with the following settings:
DEV_DSN='DSN=my_dev_dsn; UID=my_dev_user_id; PWD=my_dev_password'
PROD_DSN='DSN=my_prod_dsn; UID=my_prod_user_id; PWD=my_prod_password'
Set your environment according to your OS or Docker variable. For Linux you could enter:
export MY_ENVIORONMENT=DEV
Now consider the following settings.py:
from pydantic import BaseSettings
import os
class Settings(BaseSettings):
DSN: str
class Config():
env_prefix = f"{os.environ['MY_ENVIORONMENT']}_"
env_file = "APPNAME.cfg"
Your app would simply need to do the following:
from settings import Settings
s = Settings()
db = pyodbc.connect(s.DSN)
Bumping an old thread because I found a solution that was a bit cleaner for my use case. I was having trouble getting test specific dotenv files to load only while tests were running and when I had a local development dotenv in the project dir.
You can do something like the below where test.enviornment is a special dotenv file that is NOT an env_file path in the settings class Config. Because env vars > dotenv for BaseSettings, this will override any settings from a local .env as long as this is run in conftest.py before your settings class is imported. It also guarantees that your test environment is only active when tests are being run.
#conftest.py
from dotenv import load_dotenv
load_dotenv("tests/fixtures/test.environment", override=True)
from app import settings # singleton instance of BaseSettings class
It's really tricky to mock environment with pydantic involved.
I only achieved desired behaviour with dependency injection in fastapi and making get_settings function, which itself seems to be good practice since even documentation says to do so.
Suppose you have
...
class Settings(BaseSettings):
ENVIRONMENT: str
class Config:
env_file = ".env"
case_sensitive = True
def get_settings() -> Settings:
return Settings()
databases = {
"dev": "Development",
"test": "Testing"
}
database = databases[get_settings().ENVIRONMENT]
#app.get("/")
def read_root():
return {"Environment": database}
And in your tests you would write:
import pytest
from main import get_settings
def get_settings_override() -> Settings:
return Settings(ENVIRONMENT="dev")
#pytest.fixture(autouse=True)
def override_settings() -> None:
app.dependency_overrides[get_settings] = get_settings_override
You can use scope session if you'd like.
This would override your ENVIRONMENT variable and wouldn't touch rest of configuration variables.

struggling to test flask-dance / flask-security / flask-sqlalchemy / pytest

My application requires login to google for later use of google apis. I have flask-dance, flask-security, flask-sqlalchemy working to the point where I can do the log in and log out in my development system.
What I've been struggling with is testing of the login using pytest. I am trying force the login with the call to flask_security.login_user, but test_login fails as if nobody is logged in. I suspect this is a problem because of context setting, but I have tried a lot of different things and haven't found the magic elixir.
Unfortunately, while I have a lot of experience in software development in general and python in particular, I don't have the pytest / flask-dance / flask-security background needed to solve this.
in settings.py
class Testing():
# default database
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
TESTING = True
WTF_CSRF_ENABLED = False
# need to set SERVER_NAME to something, else get a RuntimeError about not able to create URL adapter
SERVER_NAME = 'dev.localhost'
# need a default secret key - in production replace by config file
SECRET_KEY = "<test secret key>"
# fake credentials
GOOGLE_OAUTH_CLIENT_ID = 'fake-client-id'
GOOGLE_OAUTH_CLIENT_SECRET = 'fake-client-secret'
# need to allow logins in flask-security. see https://github.com/mattupstate/flask-security/issues/259
LOGIN_DISABLED = False
in conftest.py
import pytest
from racesupportcontracts import create_app
from racesupportcontracts.dbmodel import db
from racesupportcontracts.settings import Testing
#pytest.fixture
def app():
app = create_app(Testing)
yield app
#pytest.fixture
def dbapp(app):
db.drop_all()
db.create_all()
yield app
in test_basic.py
def login_test_user(email):
from racesupportcontracts.dbmodel import db, User
from flask_security import login_user
user = User.query.filter_by(email=email).one()
login_user(user)
db.session.commit()
def test_login(dbapp):
app = dbapp
from racesupportcontracts.dbmodel import db, init_db
from racesupportcontracts import user_datastore
from flask import url_for
# init_db should create at least superadmin, admin roles
init_db(defineowner=False)
useremail = 'testuser#example.com'
with app.test_client() as client:
create_user(useremail, 'superadmin')
login_test_user(useremail)
resp = client.get('/', follow_redirects=True)
assert resp.status_code == 200
assert url_for('admin.logout') in resp.data
When you call login_user(), that modifies the flask.session object. However, when using the test client, you can only modify flask.session inside of a session transaction. It should work if you do this:
with app.test_client() as client:
with client.session_transaction() as sess:
sess["user_id"] = 1 # if you want user 1 to be logged in for this test
resp = client.get('/', follow_redirects=True)
# make whatever assertions you want
If you install the latest version of Flask-Login from GitHub, you can also use the FlaskLoginClient class to make this more readable:
# in conftest.py
from flask_login import FlaskLoginClient
#pytest.fixture
def app():
app = create_app(Testing)
app.test_client_class = FlaskLoginClient
yield app
# in test_basic.py
def test_login(app):
user = User.query.filter_by(email='testuser#example.com').one()
with app.test_client(user=user) as client:
resp = client.get('/', follow_redirects=True)
# make whatever assertions you want
Unfortunately, the author of Flask-Login refuses to publish an update of the package to PyPI, so you can't use the version of Flask-Login that is on PyPI, you have to install from GitHub. (I have no idea why he refuses to publish an update.)

Flask click command unittests - how to use testing app with "with_appcontext" decorator?

I don't know how to correctly use testing app version while unittesting (with pytest) flask cli command (with click) decorated with with_app_context decorator. This decorator replaces pytest fixture app with the "normal", development application. I use app factory pattern.
My command in a simplified version looks like this (mind #with_appcontext):
#click.command()
#click.option('--username', prompt='Username')
#click.option('--email', prompt='User E-Mail')
#click.option('--password', prompt='Password', confirmation_prompt=True, hide_input=True)
#with_appcontext # from flask.cli import with_appcontext
def createsuperuser(username, email, password):
user = User(
username=username,
email=email,
password=password,
active=True,
is_admin=True,
)
user.save()
Without #with_appcontext unittests work just fine (they get the app injected by pytest), but the command itself does not, as it needs an app context.
My extracted pytest code:
# pytest fixtures
#pytest.yield_fixture(scope='function')
def app():
"""An application for the tests."""
_app = create_app(TestConfig)
ctx = _app.test_request_context()
ctx.push()
yield _app
ctx.pop()
#pytest.yield_fixture(scope='function')
def db(app):
"""A database for the tests."""
_db.app = app
with app.app_context():
_db.create_all()
yield _db
# Explicitly close DB connection
_db.session.close()
_db.drop_all()
#pytest.mark.usefixtures('db')
class TestCreateSuperUser:
# db fixture uses app fixture, works the same if app was injected here as well
def test_user_is_created(self, cli_runner, db):
result = cli_runner.invoke(
createsuperuser,
input='johnytheuser\nemail#email.com\nsecretpass\nsecretpass'
)
assert result.exit_code == 0
assert 'SUCCESS' in result.output
# etc.
All my tests using app and db fixtures work just fine apart from these decorated ones. I'm not sure how I should workaround this with_appcontext decorator that sets the app itself.
Thank you in advance for any hint.
Inspiration taken from https://github.com/pallets/flask/blob/master/tests/test_cli.py#L254.
from flask.cli import ScriptInfo
#pytest.fixture
def script_info(app):
return ScriptInfo(create_app=lambda info: app)
In your test:
def test_user_is_created(self, cli_runner, db, script_info):
result = cli_runner.invoke(
createsuperuser,
input='johnytheuser\nemail#email.com\nsecretpass\nsecretpass',
obj=obj,
)
The accepted answer helped me figure out a solution, but did not work out of the box. Here is what worked for me in case anyone else has a similar issue
#pytest.fixture(scope='session')
def script_info(app):
return ScriptInfo(create_app=lambda: app)
def test_user_is_created(self, cli_runner, db, script_info):
result = cli_runner.invoke(
createsuperuser,
input='johnytheuser\nemail#email.com\nsecretpass\nsecretpass',
obj=script_info,
)
I had the same problem testing one of my flask commands. Although your approach works, I think it is valuable to have a look at the flask documentation here:
https://flask.palletsprojects.com/en/1.1.x/testing/#testing-cli-commands
Flask has its own test runner for cli commands that probably has a fix to our problem builtin. So instead of patching the create_app function with a lambda you could also just use app.test_cli_runner() and it works out of the box.

Flask + Peewee: where to create tables?

Instead of flask-peewee I'm using plain peewee package.
Here's the way I'm initializing the database:
import os
# just extending the BaseFlask with yaml config reader
from . extensions.flask_app import Flask
# peewee's wrapper around the database
from playhouse.flask_utils import FlaskDB
db_wrapper = FlaskDB()
# define the application factory
def create_app(env):
app = Flask(__name__)
# load config depending on the environment
app.config.from_yaml(os.path.join(app.root_path, 'config.yml'), env)
# init extensions
db_wrapper.init_app(app)
# ...
I know that I should call this to create tables:
from . models import User
db_wrapper.database.connect()
db_wrapper.database.create_tables([User])
But where do I put the table creation code, so that the database would be already initialized?
Edit
Looking at the docs I found out that I can use User.create_table(fail_silently=True) like that:
# in app/__init__.py
# define the application factory
def create_app(env):
app = Flask(__name__)
# load config depending on the environment
app.config.from_yaml(os.path.join(app.root_path, 'config.yml'), env)
# init extensions
db_wrapper.init_app(app)
create_tables();
# rest of the initialization
def create_tables():
from . models import User
User.create_table(fail_silently=True)
Is it alright to do it here? Or is there a better way/tool for this?
Edit
Figured it out. Please, see my answer below.
Update
I didn't know about the built-in CLI support in Flask. I don't know whether you should consider such an approach at all, since you can do things out of the box (see documntation).
I can utilize the flask-script package. I've done it before, just overlooked it.
Activate your virtual environment and run:
pip install flask-script
Then create manage.py file in your root directory, add these lines:
import os
from app import create_app, db_wrapper
from app.models import *
from flask_script import Manager, Shell
# create the application instance
app = create_app(os.getenv('FLASK_ENV', 'development'))
# instantiate script manager
manager = Manager(app)
def make_shell_context():
return dict(app=app, db_wrapper=db_wrapper, User=User)
#manager.command
def run_shell():
Shell(make_context = make_shell_context).run(no_ipython=True, no_bpython=True)
# here's my simple command
#manager.command
def create_tables():
User.create_table(fail_silently=True)
#manager.command
def run_tests():
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
# run it
if __name__ == '__main__':
manager.run()

Categories

Resources