404 Not Found when testing Flask application with pytest - unit-testing

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.

Related

Why flask doesn't work with gunicorn and telegram bot api?

I am using telegram bot api (telebot), flask and gunicorn.
When I use command python app.py everything is work fine but when I use python wsgi.py flask is stated on http://127.0.0.1:5000/ and bot doesn't answer and if I am using gunicorn --bind 0.0.0.0:8443 wsgi:app webhook is setting but telegram bot doesn't answer. I tried to add app.run from app.py to wsgi.py but it doesn't work
app.py
import logging
import time
import flask
import telebot
API_TOKEN = '111111111:token_telegram'
WEBHOOK_HOST = 'droplet ip'
WEBHOOK_PORT = 8443 # 443, 80, 88 or 8443 (port need to be 'open')
WEBHOOK_LISTEN = '0.0.0.0' # In some VPS you may need to put here the IP addr
WEBHOOK_SSL_CERT = 'webhook_cert.pem' # Path to the ssl certificate
WEBHOOK_SSL_PRIV = 'webhook_pkey.pem' # Path to the ssl private key
WEBHOOK_URL_BASE = "https://%s:%s" % (WEBHOOK_HOST, WEBHOOK_PORT)
WEBHOOK_URL_PATH = "/%s/" % (API_TOKEN)
logger = telebot.logger
telebot.logger.setLevel(logging.DEBUG)
bot = telebot.TeleBot(API_TOKEN)
app = flask.Flask(__name__)
# Empty webserver index, return nothing, just http 200
#app.route('/', methods=['GET', 'HEAD'])
def index():
return ''
# Process webhook calls
#app.route(WEBHOOK_URL_PATH, methods=['POST'])
def webhook():
if flask.request.headers.get('content-type') == 'application/json':
json_string = flask.request.get_data().decode('utf-8')
update = telebot.types.Update.de_json(json_string)
bot.process_new_updates([update])
return ''
else:
flask.abort(403)
# Handle all other messages
#bot.message_handler(func=lambda message: True, content_types=['text'])
def echo_message(message):
bot.reply_to(message, message.text)
# Remove webhook, it fails sometimes the set if there is a previous webhook
bot.remove_webhook()
#
time.sleep(1)
# Set webhook
bot.set_webhook(url=WEBHOOK_URL_BASE + WEBHOOK_URL_PATH,
certificate=open(WEBHOOK_SSL_CERT, 'r'))
if __name__ == "__main__":
# Start flask server
app.run(host=WEBHOOK_LISTEN,
port=WEBHOOK_PORT,
ssl_context=(WEBHOOK_SSL_CERT, WEBHOOK_SSL_PRIV),
debug=True)
wsgi.py
from app import app
if __name__ == "__main__":
app.run()
It's a bad practice to show python webservers to the world. It's not secure.
Good practice - using a reverse proxy, e.g. nginx
Chain
So the chain shoud be:
api.telegram.org -> your_domain -> your_nginx -> your_webserver -> your_app
SSL
Your ssl certificates shoud be checked on nginx level. On success just pass request to your webserver (flask or something else). It's also a tip about how to use infitity amount of bots on one host/port :)
How to configure nginx reverse proxy - you can find in startoverflow or google.
Telegram Webhooks
telebot is so complicated for using webhooks.
Try to use aiogram example. It's pretty simple:
from aiogram import Bot, Dispatcher, executor
from aiogram.types import Message
WEBHOOK_HOST = 'https://your.domain'
WEBHOOK_PATH = '/path/to/api'
bot = Bot('BOT:TOKEN')
dp = Dispatcher(bot)
#dp.message_handler()
async def echo(message: Message):
return message.answer(message.text)
async def on_startup(dp: Dispatcher):
await bot.set_webhook(f"{WEBHOOK_HOST}{WEBHOOK_PATH}")
if __name__ == '__main__':
executor.start_webhook(dispatcher=dp, on_startup=on_startup,
webhook_path=WEBHOOK_PATH, host='localhost', port=3000)
app.run(host=0.0.0.0, port=5000, debug=True)
Change port number and debug arguments based on your case.

Integrate a Dash app with a route into a proper running Flask environment

I'm struggling a bit with integrating a proper running Dash app inside a Flask website. Either I do have a problem in understanding howit works or it's only a small issue.
Each hint is welcome.
My Flask environment has this route:
#blueprint.route('/dash-test')
#login_required
def dash_test():
"""Integrate the Dash App into the page"""
server = flask.Flask(__name__)
return render_template('dashapp-test.html',
my_dashapp=my_dash_app(),
server=server
)
It's calling the function which contains the Dash app:
def my_dash_app():
app = dash.Dash(
__name__,
external_stylesheets=['https://codepen.io/chriddyp/pen/bWLwgP.css'],
)
app.layout = html.Div(id='example-div-element')
return app
Given result after send the request /dash-test:
<dash.dash.Dash object at 0x0000025909864700>
But expected is: Dash App is shown.
Something to keep in mind: when you generate a dashapp, you are adding components to an existing flask application (also called "server"). A suitable solution to this would be to build your flask app --> then build your dash app with a specific route --> then build a route in your flask app to your dashapp:
# Create a function which creates your dashapp
import dash
def create_dashapp(server):
app = dash.Dash(
server=server,
url_base_pathname='/dashapp/'
)
app.config['suppress_callback_exceptions'] = True
app.title='Dash App'
# Set the layout
app.layout = layout = html.Div('Hello Dash app')
# Register callbacks here if you want...
return app
# Create your app (or server, same thing really)
server = flask.Flask(__name__)
# Initialize by passing through your function
create_dashapp(server)
# Define index route
#server.route('/')
def index():
return 'Hello Flask app'
# Define dashapp route
#server.route('/dashapp/')
#login_required
def redirect_to_dashapp():
return redirect('/dashapp/')
# Finally run
if __name__ == '__main__':
app.run_server(debug=True)
This page from the docs might prove useful for you. It deals directly with running Dash inside a Flask app. Here is the mini example they provide:
import flask
import dash
import dash_html_components as html
server = flask.Flask(__name__)
#server.route('/')
def index():
return 'Hello Flask app'
app = dash.Dash(
__name__,
server=server,
routes_pathname_prefix='/dash/'
)
app.layout = html.Div("My Dash app")
if __name__ == '__main__':
app.run_server(debug=True)

Simple Flask example with pytest and application factory does not work

I am new to flask and I have set up a simple flask example and two tests using pytest(see here). When I let run only one test it works, but if I run both tests it does not work.
Anyone knows why? I think I am missing here some basics of how flask works.
code structure:
app/__init__.py
from flask import Flask
def create_app():
app = Flask(__name__)
with app.app_context():
from app import views
return app
app/views.py
from flask import current_app as app
#app.route('/')
def index():
return 'Index Page'
#app.route('/hello')
def hello():
return 'Hello World!'
tests/conftest.py
import pytest
from app import create_app
#pytest.fixture
def client():
app = create_app()
yield app.test_client()
tests/test_app.py
from app import create_app
def test_index(client):
response = client.get("/")
assert response.data == b"Index Page"
def test_hello(client):
response = client.get("/hello")
assert response.data == b"Hello World!"
The problem is with your registration of the routes in app/views.py when you register them with current_app as app. I'm not sure how you would apply the application factory pattern without using blueprints as the pattern description in the documentation implies they are mandatory for the pattern:
If you are already using packages and blueprints for your application [...]
So I adjusted your code to use a blueprint instead:
app/main/__init__.py:
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import views
app/views.py -> app/main/views.py:
from app.main import bp
#bp.route('/')
def index():
return 'Index Page'
#bp.route('/hello')
def hello():
return 'Hello World!'
app/__init__.py:
from flask import Flask
def create_app():
app = Flask(__name__)
# register routes with app instead of current_app:
from app.main import bp as main_bp
app.register_blueprint(main_bp)
return app
Then your tests work as intended:
$ python -m pytest tests
============================== test session starts ==============================
platform darwin -- Python 3.6.5, pytest-6.1.0, py-1.9.0, pluggy-0.13.1
rootdir: /Users/oschlueter/github/simple-flask-example-with-pytest
collected 2 items
tests/test_app.py .. [100%]
=============================== 2 passed in 0.02s ===============================

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.

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