flask test_client() unittest - flask

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.

Related

Flask-sqlalchemy 3.0.2 creating a new DB inside a Flask endpoint

I'm currently migrating my Flask app from flask-sqlalchemy 2.5.1 to 3.0.2. The app includes an endpoint that lets the client create a new database file at a selected path. Before the migration, it was achieved by simply setting the path and creating the tables in the following way:
app.config['SQLALCHEMY_DATABASE_URI'] = filepath
db.create_all()
However, the file is no longer automatically created in 3.0.2.
Question
I have been digging through the flask-sqlalchemy and sqlalchemy documentations for the past two days yet I can't find anything that would mention the changed behaviour. Creating the file before setting the config doesn't work either, as the tables are not created and the file size is 0b after calling the endpoint.
How can I create a new sqlalchemy DB file inside of a Flask endpoint?
Below is a minimal reproducible example:
server.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
TEST_DB_PATH = os.path.join(os.getcwd(), "test.db")
# create the extension
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
# create the app
app = Flask(__name__)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True
# configure the first SQLite database, in memory
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///"
# initialize the app with the extension
db.init_app(app)
with app.app_context():
db.create_all()
#app.route('/test')
def test():
# configure the second SQLite database, relative to the app instance folder
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + TEST_DB_PATH
db.create_all()
return "OK", 200
if __name__ == '__main__':
app.run()
test_server.py
import server
import os
import pytest
#pytest.fixture(scope="session")
def client():
app = server.app
return app.test_client()
def test_create_db_file(client):
# clean local test.db file if it exists
try:
os.remove(server.TEST_DB_PATH)
except OSError:
pass
ans = client.get("test")
assert ans.status_code == 200, ans.data
# this assertion fails
assert os.path.exists(
server.TEST_DB_PATH), f"DB file created at execution cannot be found at " + server.TEST_DB_PATH
assert os.path.getsize(
server.TEST_DB_PATH) > 0, "Created db file size is 0"
Running the test
py -m pytest .\test_server.py
The assertions fail with flask-sqlalchemy 3.0.2 and pass with 2.5.1.
Environment
Windows 11 x64
flask-sqlalchemy 2.2.2
sqlalchemy 1.4.46
pytest 7.2.0
python 3.9.13

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.

Pytest use django_db with rest framework

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.

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 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