Test discovery fails using DRF APITestCase but not django's TestCase - django

Using Django Rest Framework's APITestCase class, Visual Studio Code does not discover my unittest tests. I've configured vscode to use unittest and given it the path to my django app. I have toggled between jedi and ms's language server for python.
I can run the tests manually, using python manage.py test.
If I switch to using Django's provided django.test.TestCase, vscode discovers the tests and creates the adornments. I have also tried rest_framework's two other test cases: APISimpleTestCase, APITransactionTestCase and neither worked.
My test class is very simple, essentially the following:
from django.test import TestCase
# * cannot get vscode to discover tests with this
from rest_framework.test import APITestCase
service_path = "/api/v0.1/service"
# class PathLookupTests(TestCase):
class PathLookupTests(APISimpleTestCase):
def test_responding(self):
uri = "valid_uri"
resp = self.client.get(f"{service_path}/?uri={uri}")
self.assertEqual(resp.status_code, 200)
In the Python Test Log I saw the following traceback once, but cannot repeat it:
File "/Users/bfalk/miniconda3/envs/web-server-eval/lib/python3.8/site-packages/django/test/testcases.py", line 1123, in setUpClass
super().setUpClass()
File "/Users/bfalk/miniconda3/envs/web-server-eval/lib/python3.8/site-packages/django/test/testcases.py", line 197, in setUpClass
cls._add_databases_failures()
File "/Users/bfalk/miniconda3/envs/web-server-eval/lib/python3.8/site-packages/django/test/testcases.py", line 218, in _add_databases_failures
cls.databases = cls._validate_databases()
File "/Users/bfalk/miniconda3/envs/web-server-eval/lib/python3.8/site-packages/django/test/testcases.py", line 204, in _validate_databases
if alias not in connections:
TypeError: argument of type 'ConnectionHandler' is not iterable

After a bit more digging, I now see that django's test runner does not work in vscode. https://github.com/microsoft/vscode-python/issues/73
The solution, at least for me, was to use pytest-django
Here's the example above but using pytest-django (after setting up my pytest.ini file to point at my django app)
service_path = "/api/v0.1/service"
def test_responding(client):
uri = "valid_uri"
resp = client.get(f"{service_path}/?uri={uri}")
assert resp.status_code == 200

Related

SQLAlchemy doesn't let me set up Flask apps multiple times in a test fixture

I am writing a Flask application that uses SQLAlchemy for its database backend.
The Flask application is created with an app factory called create_app.
from flask import Flask
def create_app(config_filename = None):
app = Flask(__name__)
if config_filename is None:
app.config.from_pyfile('config.py', silent=True)
else:
app.config.from_mapping(config_filename)
from .model import db
db.init_app(app)
db.create_all(app=app)
return app
The database model consists of a single object called Document.
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Document(db.Model):
id = db.Column(db.Integer, primary_key=True)
document_uri = db.Column(db.String, nullable=False, unique=True)
I am using pytest to do unit testing. I create a pytest fixture called app_with_documents that calls the application factory to create an application and adds some Document objects to the database before the test is run, then empties out the database after the unit test has completed.
import pytest
from model import Document, db
from myapplication import create_app
#pytest.fixture
def app():
config = {
'SQLALCHEMY_DATABASE_URI': f"sqlite:///:memory:",
'TESTING': True,
'SQLALCHEMY_TRACK_MODIFICATIONS': False
}
app = create_app(config)
yield app
with app.app_context():
db.drop_all()
#pytest.fixture
def app_with_documents(app):
with app.app_context():
document_1 = Document(document_uri='Document 1')
document_2 = Document(document_uri='Document 2')
document_3 = Document(document_uri='Document 3')
document_4 = Document(document_uri='Document 4')
db.session.add_all([document_1, document_2, document_3, document_4])
db.session.commit()
return app
I have multiple unit tests that use this fixture.
def test_unit_test_1(app_with_documents):
...
def test_unit_test_2(app_with_documents):
...
If I run a single unit test everything works. If I run more than one test, subsequent unit tests crash at the db.session.commit() line in the test fixture setup with "no such table: document".
def do_execute(self, cursor, statement, parameters, context=None):
> cursor.execute(statement, parameters)
E sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: document [SQL: 'INSERT INTO document (document_uri) VALUES (?)'] [parameters: ('Document 1',)] (Background on this error at: http://sqlalche.me/e/e3q8)
What I expect is that each unit test gets its own brand-new identical prepopulated database so that all the tests would succeed.
(This is an issue with the database tables, not the unit tests. I see the bug even if my unit tests consist of just pass.)
The fact that the error message mentions a missing table makes it look like the db.create_all(app=app) in create_app is not being called after the first unit test runs. However, I have verified in the debugger that this application factory function is called once for every unit test as expected.
It is possible that my call to db.drop_all() is an incorrect way to clear out the database. So instead of an in-memory database, I tried creating one on disk and then deleting it as part of the test fixture cleanup. (This is the technique recommended in the Flask documentation.)
#pytest.fixture
def app():
db_fd, db_filename = tempfile.mkstemp(suffix='.sqlite')
config = {
'SQLALCHEMY_DATABASE_URI': f"sqlite:///{db_filename}",
'TESTING': True,
'SQLALCHEMY_TRACK_MODIFICATIONS': False
}
yield create_app(config)
os.close(db_fd)
os.unlink(db_filename)
This produces the same error.
Is this a bug in Flask and/or SQLAlchemy?
What is the correct way to write Flask test fixtures that prepopulate an application's database?
This is Flask 1.0.2, Flask-SQLAlchemy 2.3.2, and pytest 3.6.0, which are all the current latest versions.
In my conftest.py I was importing the contents of model.py in my application like so.
from model import Document, db
I was running the unit tests in Pycharm using Pycharm's pytest runner. If instead I run tests from the command line with python -m pytest I see the following error
ModuleNotFoundError: No module named 'model'
ERROR: could not load /Users/wmcneill/src/FlaskRestPlus/test/conftest.py
I can get my tests running from the command line by fully-qualifying the import path in conftest.py.
from myapplication.model import Document, db
When I do this all the unit tests pass. They also pass when I run the unit tests from inside Pycharm.
So it appears that I had incorrectly written an import statement in my unit tests. However, when I ran those unit tests via Pycharm, instead of seeing an error message about the import, the scripts launched but then had weird SQL errors.
I still haven't figured out why I saw the strange SQL errors I did. Presumably something subtle about the way global state is being handled. But changing the import line fixes my problem.

Django test print or log failure

I have a django_rest_framework test (the problem is the same with a regular django test) that looks like this:
from rest_framework.test import APITestCase
class APITests(APITestCase):
# tests for unauthorized access
def test_unauthorized(self):
...
for api in apipoints:
response = self.client.options(api)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
I have a url that fails, the terminal shows this:
FAIL: test_unauthorized (app.misuper.tests.APITests)
---------------------------------------------------------------------- Traceback (most recent call last): File
"/home/alejandro/...",
line 64, in test_unauthorized
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) AssertionError: 200 != 403
Ok, how can I know which url failed the test? I am iterating through all urls that require login, that is many urls, how can I print the one that failed the test?
For a simple quick-fix, you can pass the apipoint in the third parameter of the assertion method:
>>> from unittest import TestCase
>>> TestCase('__init__').assertEqual(1, 2, msg='teh thing is b0rked')
AssertionError: teh thing is b0rked
In the spirit of unit testing, these should really be each different test methods rather than only one test method with the loop. Check out nose_parameterized for help with making that more DRY. You'll decorate the test method like this:
from nose_parameterized import parameterized
#parameterized.expand(apipoints)
def test_unauthorized(self, apipoint):
response = self.client.options(apipoint)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
The decorator will generate different test methods for each endpoint, so that they can pass/fail independently of one another.
Although this package has nose in the name, it's also compatible with other runners such as unittest and py.test.

Pycharm django tests settings

Having troubles running tests in PyCharm.
manage.py test works fine.
But if I run test in PyCharm Django Test getting following error:
AttributeError: 'module' object has no attribute 'ROOT_URLCONF'
Django tests Run\Debug configuration:
DJANGO_SETTINGS_MODULE=project.settings.test
Django Preferences
Settings: project/settings/test.py
Manage Script: project/manage.py
Test
from django.test import SimpleTestCase
from rest_framework import status
class AccountTestCase(SimpleTestCase):
def test_current_account(self):
url = '/api/accounts/current/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_301_MOVED_PERMANENTLY)
StackTrace
Error
packages/django/core/handlers/base.py", line 113, in get_response
urlconf = settings.ROOT_URLCONF
File "/lib/python2.7/site-packages/django/conf/__init__.py", line 56, in __getattr__
return getattr(self._wrapped, name)
File "/lib/python2.7/site-packages/django/conf/__init__.py", line 173, in __getattr__
return getattr(self.default_settings, name)
AttributeError: 'module' object has no attribute 'ROOT_URLCONF'
Would appreciate any help.
Ok got it.
You have to mark root folder in PyCharm as "sources root"
Then I guess it adds it to PYTHONPATH
Verify the Pycharm Tools menu has an item "Run manage.py Task...". If not, configuration changes are needed in File > Settings > Languages & Frameworks > Django. Additionally, if using a remote database, permission to create databases is required.
PyCharm Django projects require some setup to fully use all the IDE features. Many actions run fine, but Django testing does not. When running test.py, the following console messages indicate additional project setup is needed:
There is no such settings file settings
AttributeError: module 'django.conf.global_settings' has no attribute 'ROOT_URLCONF'

Django, PostgreSQL, set_autocommit and test cases

It looks like whenever I use transaction.set_autocommit(False) in a test case, I get the following stack trace:
transaction.set_autocommit(False)
File "/Users/btoueg/src/python/python3.3.3_django1.6.1/lib/python3.3/site-packages/django/db/transaction.py", line 133, in set_autocommit
return get_connection(using).set_autocommit(autocommit)
File "/Users/btoueg/src/python/python3.3.3_django1.6.1/lib/python3.3/site-packages/django/db/backends/__init__.py", line 331, in set_autocommit
self.validate_no_atomic_block()
File "/Users/btoueg/src/python/python3.3.3_django1.6.1/lib/python3.3/site-packages/django/db/backends/__init__.py", line 360, in validate_no_atomic_block
"This is forbidden when an 'atomic' block is active.")
django.db.transaction.TransactionManagementError: This is forbidden when an 'atomic' block is active.
Is this normal behavior ? It looks like Django’s TestCase class wraps each test in a transaction for performance reasons.
So the question is : how do I test my code in a Django Testcase if it already uses a transaction ?
I'm using Django 1.6 with PostgreSQL 9.2
Django TestCase inherits from TransactionTestCase.
According to the doc, TestCase does basically the same as TransactionTestCase, but surrounds every test with a transaction (...). You have to use TransactionTestCase, if you need transaction management inside a test.
My situation is a little bit different because my test class is derived from DRF APITestCase. So in order to check transaction management in my test case, I did the following:
from rest_framework.test import APITestCase
from django.test import TestCase
class MyTestCase(APITestCase):
def _fixture_setup(self):
super(TestCase, self)._fixture_setup()
def _fixture_teardown(self):
super(TestCase, self)._fixture_teardown()
...

Emulating an app with models in a django unittest

Im writing some code which retrieves info about installed apps, especially defined models, and then does stuff based on that information, but Im having some problems writing a clean, nice unittest. Is there a way to emulate or add an app in unittests without have to run manage.py startproject, manage.py startapp in my testsfolder to have a test app available for unittests?
Sure, try this on for size:
from django.conf import settings
from django.core.management import call_command
from django.test.testcases import TestCase
from django.db.models import loading
class AppTestCase(TestCase):
'''
Adds apps specified in `self.apps` to `INSTALLED_APPS` and
performs a `syncdb` at runtime.
'''
apps = ()
_source_installed_apps = ()
def _pre_setup(self):
super(AppTestCase, self)._pre_setup()
if self.apps:
self._source_installed_apps = settings.INSTALLED_APPS
settings.INSTALLED_APPS = settings.INSTALLED_APPS + self.apps
loading.cache.loaded = False
call_command('syncdb', verbosity=0)
def _post_teardown(self):
super(AppTestCase, self)._post_teardown()
if self._source_installed_apps:
settings.INSTALLED_APPS = self._source_installed_apps
self._source_installed_apps = ()
loading.cache.loaded = False
Your test case would look something like this:
class SomeAppTestCase(AppTestCase):
apps = ('someapp',)
In case you were wondering why, I did an override of _pre_setup() and _post_teardown() so I don't have to bother with calling super() in setUp() and tearDown() in my final test case. Otherwise, this is what I pulled out of Django's test runner. I whipped it up and it worked, although I'm sure that, with closer inspection, you can further optimize it and even avoid calling syncdb every time if it won't conflict with future tests.
EDIT:
So I seem to have gotten out of my way, thinking you need to dynamically add new models. If you've created an app for testing purposes only, here's what you can do to have it discovered during your tests.
In your project directory, create a test.py file that will contain your test settings. It should look something like this:
from settings import *
# registers test app for discovery
INSTALLED_APPS += ('path.to.test.app',)
You can now run your tests with python manage.py test --settings=myproject.test and your app will be in the installed apps.