Pytest use django_db with rest framework - django

I am trying to get a simple test to work against the real django_db not the test database using the django rest framework.
Basic test setup:
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
#pytest.mark.django_db
def test_airport_list_real():
client = APIClient()
response = client.get(reverse('query_flight:airports-list'))
assert response.status_code == 200
assert len(response.json()) > 0
Running this test I get:
___________________________ test_airport_list_real ____________________________
#pytest.mark.django_db
def test_airport_list_real():
client = APIClient()
response = client.get(reverse('query_flight:airports-list'))
assert response.status_code == 200
> assert len(response.json()) > 0
E assert 0 > 0
E + where 0 = len([])
E + where [] = functools.partial(<bound method Client._parse_json of <rest_framework.test.APIClient object at 0x000001A0AB793908>>, <Response status_code=200, "application/json">)()
E + where functools.partial(<bound method Client._parse_json of <rest_framework.test.APIClient object at 0x000001A0AB793908>>, <Response status_code=200, "application/json">) = <Response status_code=200, "application/json">.json
query_flight\tests\query_flight\test_api.py:60: AssertionError
When just running in the shell using pipenv run python manage.py shell I get the expected results:
In [1]: from django.urls import reverse
In [2]: from rest_framework.test import APIClient
In [3]: client = APIClient()
In [4]: response = client.get(reverse('query_flight:airports-list'))
In [5]: len(response.json())
Out[5]: 100
Using the following packages:
pytest-django==3.2.1
pytest [required: >=2.9, installed: 3.5.1]
djangorestframework==3.8.2
django [required: >=1.8, installed: 2.0.5]
Is there anyway to get pytest to access the real database in this way?

The django_db marker is only responsible to provide a connection to the test database for the marked test. The django settings passed to pytest-django are solely responsible for the selection of database used in the test run.
You can override the database usage in pytest-django by defining the django_db_setup fixture. Create a conftest.py file in the project root if you don't have it yet and override the db configuration:
# conftest.py
import pytest
#pytest.fixture(scope='session')
def django_db_setup():
settings.DATABASES['default'] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'path/to/dbfile.sqlite3',
}
However, you shouldn't use the real database in tests. Make a dump of your current db to get a snapshot of test data (python manage.py dumpdata > testdata.json) and load it into an empty test database to populate it before the test run:
# conftest.py
import pytest
from django.core.management import call_command
#pytest.fixture(scope='session')
def django_db_setup(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
call_command('loaddata', 'testdata.json')
Now, you can't possibly corrupt your real db when running tests; any future changes in real db will not cause the tests to fail (for example, when some data was deleted) and you always have a deterministic state on each test run. If you need some additional test data, add it in JSON format to testdata.json and your tests are good to go.
Source: Examples in pytest-django docs.

You've got a couple options. Using Django's TestClient or DRF's APIClient will use the test database and local version of your app by default. To connect to your live API, you could use a library like Requests to perform HTTP requests, then use those responses in your tests:
import requests
#pytest.mark.django_db
def test_airport_list_real():
response = requests.get('https://yourliveapi.biz')
assert response.status_code == 200
assert len(response.json()) > 0
Just be extra careful to perform exclusively read-only tests on that live database.

Related

Cannot Authenticate Test Requests with Both User Token and API Key in Django Rest Framework

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)

404 Not Found when testing Flask application with pytest

This simple web service works if I run it by hand but in my unit tests I get a 404 not found page as my response, preventing me to properly test the application.
normal behavior:
Folder structure:
/SRC
--web_application.py
/UNIT_TESTS
--test_wab_application.py
web_application.py
from flask import Flask, request, jsonify, send_from_directory
from python.Greeting import Greeting
application = Flask(__name__)
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='mega_developer',
DATABASE=os.path.join(app.instance_path, 'web_application.sqlite'),
)
try:
os.makedirs(app.instance_path)
except OSError:
pass
return app
#application.route('/greetings', methods=['GET', 'POST'])
def hello():
# GET: for url manipulation #
if request.method == 'GET':
return jsonify(hello = request.args.get('name', 'world', str))
test_web_application.py
import tempfile
import pytest
import web_application
class TestWebApplication:
app = web_application.create_app() # container object for test applications #
#pytest.fixture
def initialize_app(self):
app = web_application.create_app()
app.config['TESTING'] = True
app.config['DEBUG'] = False
app.config['WTF_CSRF_ENABLED'] = False
app.config['DATABASE'] = tempfile.mkstemp()
app.testing = True
self.app = app
def test_hello_get(self, initialize_app):
with self.app.test_client() as client:
response = client.get('/greetings?name=Rick Sanchez')
assert response.status_code == 200
test results (most relevant part only):
Launching pytest with arguments test_web_application.py::TestWebApplication::test_hello_get in C:\Users\Xrenyn\Documents\Projekte\Studium_Anhalt\QA&Chatbots Exercises\Exercise 2 - Web Service Basics\UNIT_TESTS
============================= test session starts =============================
platform win32 -- Python 3.8.0, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 -- C:\Users\Xrenyn\Documents\Projekte\Studium_Anhalt\QA&Chatbots Exercises\Exercise 2 - Web Service Basics\VENV\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\Xrenyn\Documents\Projekte\Studium_Anhalt\QA&Chatbots Exercises\Exercise 2 - Web Service Basics\UNIT_TESTS
collecting ... collected 1 item
test_web_application.py::TestWebApplication::test_hello_get FAILED [100%]
test_web_application.py:21 (TestWebApplication.test_hello_get)
404 != 200
Expected :200
Actual :404
So far I have tested various alternative routing paths for the client.get() method in test-web_application.py , including combinations like '/SRC/greetings?name=Rick Sanchez' or '../SRC/greetings?name=Rick Sanchez', but all to no different effect.
Do you have any idea on what I might be doing wrong or how I could get access to my web services' functions from within unit tests?
I think the problem is that you are creating two Flask instances. One with the name application that you add hello route to, and the second one using the create_app function. You need to create a test client using the application instance (the one you added the hello route to).
Can you import application and then obtain the client using application.test_client()?
Sample solution:
import pytest
from web_application import application
#pytest.fixture
def client():
with application.test_client() as client:
yield client
class TestSomething:
def test_this(self, client):
res = client.get('/greetings?name=Rick Sanchez')
assert res.status_code == 200
Checkout the official docs on testing.

flask test_client() unittest

I am new to programming in general and this is my first web application in python (flask, sqlalchemy, wtforms, etc). I have been using the realpython.com course 2 as my study material on this subject. I have gotten to the point where i am learning about unit testing and i having trouble getting it to work correctly. I have compared the course example to the examples i found online and i am not seeing the issue with my code.
The problem i am encountering is that the test.py script correctly creates my test.db database but when it attempts to insert a test customer and it puts it into my production db (madsenconcrete.db) instead of my test db (test.db). If i remove the production db from the script directory it will raise this error when it cant find the db because its looking for madsenconcrete.db not test.db.
OperationalError: (sqlite3.OperationalError) no such table: customer [SQL: u'INSERT INTO customer (name, email, telephone, created_date) VALUES (?, ?, ?, ?)'] [parameters: ('Acme Company', 'acme#domain.com', '6125551000', '2016-01-03')]
I am not sure how to troubleshoot this issue. I have doing a lot of stare and compares and i do not see the difference.
import os
import unittest
import datetime
import pytz
from views import app, db
from _config import basedir
from models import Customer
TEST_DB = 'test.db'
class AllTests(unittest.TestCase):
############################
#### setup and teardown ####
############################
# executed prior to each test
def setUp(self):
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, TEST_DB)
app.config['SQLALCHEMY_ECHO'] = True
self.app = app.test_client()
db.create_all()
# executed after each test
def tearDown(self):
db.session.remove()
db.drop_all()
# each test should start with 'test'
def test_customer_setup(self):
new_customer = Customer("Acme Company", "acme#domain.com", "6125551000",
datetime.datetime.now(pytz.timezone('US/Central')))
db.session.add(new_customer)
db.session.commit()
if __name__ == "__main__":
unittest.main()
There would be an extensive amount of code i would have to paste so show all the dependencies. You can find the source code here.
https://github.com/ande0581/madsenconcrete
Thanks
Ultimately, the problem is that you are creating your db object from an already configured app:
# config
app = Flask(__name__)
app.config.from_object('_config')
db = SQLAlchemy(app)
If you use the create_app pattern (documented in more detail in this answer) you will be able to alter the configuration you are loading for your test application.

Django Lettuce built-in server 500 response

I am running the Lettuce built-in server to test that it returns a given reponse however, it shows a 500 response.
My features file:
Feature: home page loads
Scenario: Check that home page loads with header
Given I access the home url
then the home page should load with the title "Movies currently showing"
My steps file:
#step(u'Given I access the home url')
def given_i_access_the_home_url(step):
world.response = world.browser.get(django_url('/'))
sleep(10)
#step(u'then the home page should load with the title "([^"]*)"')
def then_the_home_page_should_load_with_the_title_group1(step, group1):
assert group1 in world.response
My Terrains file:
from django.core.management import call_command
from django.test.simple import DjangoTestSuiteRunner
from lettuce import before, after, world
from logging import getLogger
from selenium import webdriver
try:
from south.management.commands import patch_for_test_db_setup
except:
pass
logger = getLogger(__name__)
logger.info("Loading the terrain file...")
#before.runserver
def setup_database(actual_server):
'''
This will setup your database, sync it, and run migrations if you are using South.
It does this before the Test Django server is set up.
'''
logger.info("Setting up a test database...")
# Uncomment if you are using South
# patch_for_test_db_setup()
world.test_runner = DjangoTestSuiteRunner(interactive=False)
DjangoTestSuiteRunner.setup_test_environment(world.test_runner)
world.created_db = DjangoTestSuiteRunner.setup_databases(world.test_runner)
call_command('syncdb', interactive=False, verbosity=0)
# Uncomment if you are using South
# call_command('migrate', interactive=False, verbosity=0)
#after.runserver
def teardown_database(actual_server):
'''
This will destroy your test database after all of your tests have executed.
'''
logger.info("Destroying the test database ...")
DjangoTestSuiteRunner.teardown_databases(world.test_runner, world.created_db)
#before.all
def setup_browser():
world.browser = webdriver.Firefox()
#after.all
def teardown_browser(total):
world.browser.quit()
What could be the problem with the server, why a 500 response error?
I managed to find what the problem is, the migrations were not running on syncdb

Problems with Django test runner and test client login with authentication backend

Using the shell, I can do this:
>>> from django.test.client import Client
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> c = Client()
>>> c.login(username="dev", password="password")
True
>>> r = c.get('/')
>>> r.status_code
200
Now with this in the test.py file:
from django.test.client import Client
__test__ = {"doctest": """
>>> c = Client()
>>> c.login(username='dev', password='password')
True
>>> r = c.get('/')
>>> r.status_code
200
"""}
I get this output:
Failed example:
c.login(username="dev", password="password")
Expected:
True
Got:
False
------------------------------------------------------
Failed example:
r.status_code
Expected:
200
Got:
302
I've looked all over the internet and I can't find anything that helps with this situation. Any ideas?
On a similar note, I've commented out: from django.views.decorators.debug import sensitive_post_parameters and all #sensitive_post_parameters() decorators from my code because each time I run ./manage.py test app django complains:
Could not import app.views. Error was: No module named debug
Removing this decorator and import statement allows it to move forward.
Im very much lost and I need StackOverflow! Thanks everyone.
sensitive_post_parameters is a new feature in Django 1.4, so if you're running Django 1.3 or earlier then the import will fail.
I believe that the commands you tried in the shell were run on the normal database. When you run your doc tests, Django will set up a test database. It looks like your user dev isn't in the test database when you run the doc tests, so the login attempt fails. One option is to create the User with User.objects.create_user before you attempt the login. Another option is to use fixtures.
With Django, I would recommend writing unit tests instead of doc tests. One big advantage is that it's easy to include fixtures to load initial data (e.g. users) into the test database. Another is that Django takes care of refreshing the database between unit tests.