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.
Related
My project requires two authentication methods for some endpoints:
A global API key using this app.
A user authentication token using the default one provided by DRF
All works fine when working with the API using Postman or an iOS app, but I couldn't make the authentication work in my tests. The user auth works fine, but the global key fails.
This is how the HTTP headers look in Postman:
X-Api-Key: KEY_VALUE
Authorization: Token TOKEN_VALUE
I tried to experiment with the code in the shell and authentication worked fine using the exact same code used in the test! Only in the tests it fails so I'm not really sure how to debug this further.
Edit:
You can see a complete project on github.
Test output:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test (app.tests.MyTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File ".../authtest/app/tests.py", line 21, in test
self.assertEqual(response.status_code, status.HTTP_200_OK)
AssertionError: 403 != 200
----------------------------------------------------------------------
Ran 1 test in 0.112s
FAILED (failures=1)
Destroying test database for alias 'default'...
When I open the shell with python manage.py shell and copy and paste the test code:
>>> from rest_framework.test import (APITestCase, APIRequestFactory, force_authenticate)
>>> from rest_framework import status
>>> from accounts.models import User
>>> from app.views import MyView
>>> user = User.objects.create(email="test#test.com")
>>> user.set_password('1234')
>>> factory = APIRequestFactory()
>>> API_KEY = "KIkKSSz7.ziURxOZv8e66f28eMLYwPNs7eEhrNtYl"
>>> headers = {"HTTP_X_API_KEY": API_KEY}
>>> request = factory.get('/myview', **headers)
>>> force_authenticate(request, user=user)
>>> response = MyView.as_view()(request)
>>> response.status_code
200
Also making the request with postman works. Any idea what's going on here?
The problem is the hardcoded API_KEY you are passing in your test which is not a valid key resulting in status 403. I think the reason might be django uses a separate test database when running tests and djangorestframework-api-key uses APIKey model to generate api_key.
You can confirm this by:
Removing HasAPIKey from permission classes in MyView and running test again.
or by passing empty string as API_KEY in your test resulting in same
error you are getting now.
so what i might suggest is generating a new valid api_key and passing it instead of hardcoded key.
Here i am sharing an updated test file of how you can carry out your test
from rest_framework.test import (APITestCase, APIRequestFactory, force_authenticate)
from rest_framework import status
from accounts.models import User
from .views import MyView
from rest_framework_api_key.models import APIKey
class MyTests(APITestCase):
def test(self):
user = User.objects.create(email="test#test.com")
user.set_password('1234')
user.save()
factory = APIRequestFactory()
api_key, key = APIKey.objects.create_key(name="my-remote-service")
headers = {"HTTP_X_API_KEY": key}
request = factory.get('/myview', **headers)
force_authenticate(request, user=user)
response = MyView.as_view()(request)
self.assertEqual(response.status_code, status.HTTP_200_OK)
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
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.
I want to write a test that will test change the password in the application. I use the django-allauth. For testing, I use django-WebTest.
When I run my code, I get the message:
FAILED (errors=1)
Destroying test database for alias 'default'...
mark#mariusz-K73E:~/myapp$ python manage.py test users
Creating test database for alias 'default'...
.E
======================================================================
ERROR: test_password_change_use_template (myapp.users.tests.ChangePasswordTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/mark/myapp/users/tests.py", line 16, in test_password_change_use_template
password_change.form['oldpassword'] = "test123"
File "/home/mark/.virtualenvs/urlop/local/lib/python2.7/site-packages/webtest/response.py", line 56, in form
"You used response.form, but no forms exist")
TypeError: You used response.form, but no forms exist
My code:
from django_webtest import WebTest
from django_dynamic_fixture import G
from users.models import User
from django.core.urlresolvers import reverse
class ChangePasswordTest(WebTest):
def setUp(self):
self.user = G(User)
def test_password_change_code(self):
password_change = self.app.get(reverse('account_change_password'), user=self.user)
def test_password_change_use_template(self):
password_change = self.app.get(reverse('account_change_password'), user=self.user)
password_change.form['oldpassword'] = "test123"
password_change.form['password1'] = "test456"
password_change.form['password2'] = "test456"
password_change.form.submit()
self.assertRedirects(password_change, reverse('change_password'))
WebTest tests the rendered content. There is no form found in the HTML (maybe the form tag is incorrect or missing).
If it's working in your manual test. You can try to print the response to see what's different:
def test_password_change_use_template(self):
response = self.app.get(reverse('account_change_password'), user=self.user)
print response
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.