Django, PostgreSQL, set_autocommit and test cases - django

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

Related

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

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

pytest-django won't allow database access even with mark

I'm having a difficult time figuring out what is wrong with my setup. I'm trying to test a login view, and no matter what I try, I keep getting:
Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixtures to enable it.
My test:
import pytest
from ..models import User
#pytest.mark.django_db
def test_login(client):
# If an anonymous user accesses the login page:
response = client.get('/users/login/')
# Then the server should respond with a successful status code:
assert response.status_code == 200
# Given an existing user:
user = User.objects.get(username='user')
# If we attempt to log into the login page:
response = client.post('/users/login/', {'username': user.username, 'password': 'somepass'})
# Then the server should redirect the user to the default redirect location:
assert response.status_code == 302
My conftest.py file, in the same tests directory:
import pytest
from django.core.management import call_command
#pytest.fixture(autouse=True)
def django_db_setup(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
call_command('loaddata', 'test_users.json')
My pytest.ini file (which specifies the correct Django settings file):
[pytest]
DJANGO_SETTINGS_MODULE = config.settings
I'm stumped. I've tried using scope="session" like in the documentation together with either an #pytest.mark.django_db mark, a db fixture (as a parameter to the test function), or both with no luck. I've commented out each line of the test to figure out which one was triggering the problem, but couldn't figure it out. I could only get the test to run at all if I removed all db-dependent fixtures/marks/code from the test and had a simple assert True. I don't believe the issue is in my Django settings, as the development server runs fine and is able to access the database.
What am I missing here?
Apparently this is a case of "deceiving exception syndrome". I had a migration that created groups with permissions, and since tests run all migrations at once, the post-migrate signal that creates the permissions that migration depends on was never run before getting to that migration.
It seems that if there is any database-related error before the actual tests start running, this exception is raised, which makes it very difficult to debug exactly what is going wrong. I ended up updating my migration script to manually cause permissions to be created so that the migration could run, and the error went away.
You can add below code in your conftest.py as per the official documentation to allow DB access without django_db marker.
#pytest.fixture(autouse=True)
def enable_db_access_for_all_tests(db):
pass
Ref: https://pytest-django.readthedocs.io/en/latest/faq.html#how-can-i-give-database-access-to-all-my-tests-without-the-django-db-marker

Can I use fixtures to create a test from production database?

I have found a strange bug in my Django application. I have a view accessible on /people/<pk>. In tests it works just fine, but in production I have stumbled over a bug at several (just a few!) pk's: Reverse for 'single-person' with arguments '('',)' not found. 1 pattern(s) tried: ['people/(\\d+)/?$']. Since I couldn't catch it with my tests, I'd like to create another test, with the current state of the database, to debug. Also, to prevent future situations like this, I'd always like to be able to run my tests with a copy of the production database, to minimize the chances something goes wrong with the actual data.
I thought it would be as straightforward as
manage.py dumdata --output db-test.json
and then
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test import Client
from people.models import Person
class TestPeopleWithMyData(StaticLiveServerTestCase):
fixtures = ['db-test.json']
def setUp(self):
self.client = Client()
self.client.force_login(user='test_user')
def test_person(self):
for person in Person.objects.all():
print('Testing %s: ' % person, end='')
r = self.client.get('/people/%d' % person.pk)
print(r)
...
However this attempt fails:
======================================================================
ERROR: setUpClass (people.tests.test_my_data.TestPeopleWithMyData)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/username/Documents/python_projects/project/venv/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py", line 178, in __get__
rel_obj = getattr(instance, self.cache_name)
AttributeError: 'Person' object has no attribute '_added_by_cache'
(I have a field named added_by in my Person model.) And then it goes on to say django.contrib.auth.models.DoesNotExist: Problem installing fixture '/Users/username/Documents/python_projects/project/db-test.json': User matching query does not exist.
Since I know that this exactly database works just fine, it doesn't look right to me. So before I dive into debugging this problem, I would like to understand if what I am doing is maybe fundamentally wrong and I shouldn't be able to create test fixtures just like that. Or am I missing some simple mistake?
I think you need to do before running test
python manage.py makemigrations
python manage.py migrate
In your fixture file you have '_added_by_cache' that's not in your model.

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.

IndexError while making a query in a TestCase

I have a very simple test as follows:
import models
from django.test import TestCase
MyViewTest(TestCase):
def setUp(self):
self.trip = models.Trip.objects.order_by('?')[0]
def test_something(self):
# Blah Blah
whenever i run test it throws the error mentioned below:
Traceback (most recent call last):
File "/home/amyth/Projects/test/trips/tests.py", line 8, in setUp
self.trip = models.Trip.objects.order_by('?')[0]
File "/usr/local/lib/python2.7/dist-packages/django/db/models/query.py", line 207, in __getitem__
return list(qs)[0]
IndexError: list index out of range
I also tried changing the query to models.Trip.objects.all()[0] and it still throws the same error. What's strange is if I use any of the above queries within the shell it works. Then howcome it is not working within a test ?
See the documentation on testing in django. A new 'test' database is created, and your 'production' database is not used. Unless you create Trip entries in the TestCase setUp method, it is empty. Also, after each TestCase is run, the database is truncated, so if you need to use the Trips in multiple TestCases, you will need to create the database entry for it in each TestCase setUp.