I have a pytest test case like below:
#pytest.fixture
def app():
app = SampleApp().setup()
yield app
#pytest.fixture
def client(app):
return app.test_client()
def test_get_nonexistent_user(client, app):
with pytest.raises(NotFound) as err:
user = client.get('/users/5d4001f799556f10b7462e20')
assert err.type is NotFound
What I have read from pytest documentation I should be able to catch this exception. What I receive instead is as below:
1 failed in 0.25 seconds
And I see NotFound error in traceback error, so I'm sure code is raising the exception I expect it to.
Related
I am struggling to write test cases that will trigger an Exception within one of my FastAPI routes. I was thinking pytest.Raises would do what I intend, however that by itself doesn't seem to be doing what I thought it would.
Since the TestClient runs the API client pretty much separately, it makes sense that I would have this issue - that being said, I am not sure what the best practice is to ensure a high code coverage in testing.
Here is my test function:
def test_function_exception():
with pytest.raises(Exception):
response = client.post("/")
assert response.status_code == 400
and here is the barebones route that I am hitting:
#router.post("/")
def my_function():
try:
do_something()
except Exception as e:
raise HTTPException(400, "failed to do something")
Is there anyway that I can catch this Exception without making changes to the API route? If changes are needed, what are the changes required to ensure thorough testing?
Following the discussion below the question, I assembled a working example for you. Typically, if you can't logically hit your except block, you can ensure that the try block is raising an Exception by monkey patching the function that is tried, and replace it with something that definitely will raise an exception. In the below example, I will change the function do_something() that is defined in app.py with replace_do_something() that will just raise an Exception when called.
You can put the following files in the same folder (not a module) and try it for yourself:
File app.py:
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
import pytest
app = FastAPI()
def do_something():
return "world"
#app.get("/myroute")
async def myroute():
try:
text = do_something()
return {"hello": text}
except Exception:
raise HTTPException(400, "something went wrong")
File test_app.py:
import pytest
from fastapi.testclient import TestClient
from app import app
client = TestClient(app)
def replace_do_something():
raise Exception()
return
def test_read_main(monkeypatch: pytest.MonkeyPatch):
response = client.get("/myroute")
assert response.status_code == 200
assert response.json() == {"hello": "world"}
def test_read_main_with_error(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr("app.do_something", replace_do_something)
# Here we replace any reference to do_something
# with replace_do_something. Note the 'app.' prefix!
response = client.get("/myroute")
assert response.status_code == 400
assert response.json() == {"detail": "something went wrong"}
You can call the test_app.py file with pytest (I also have pytest-cov installed to demonstrate the 100% coverage):
(venv) jarro#MacBook-Pro-van-Jarro fastapi-github-issues % pytest --cov=app SO/pytestwithmock/test_app.py
===================================================== test session starts =====================================================
platform darwin -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: /Users/jarro/Development/fastapi-github-issues
plugins: anyio-3.6.1, cov-3.0.0
collected 2 items
SO/pytestwithmock/test_app.py .. [100%]
---------- coverage: platform darwin, python 3.10.5-final-0 ----------
Name Stmts Miss Cover
----------------------------------------------
SO/pytestwithmock/app.py 13 0 100%
----------------------------------------------
TOTAL 13 0 100%
====================================================== 2 passed in 0.19s ======================================================
This is my flask unit test setup, I launch an app_instance for all tests and rollback for each function to make sure the test DB is fresh and clean.
#fixture(scope="session", autouse=True)
def app_instance():
app = setup_test_app()
create_test_user_records()
return app
#commit_test_data
def create_test_user_records():
db.session.add_all([Test_1, Test_2, Test_3])
#fixture(scope="function", autouse=True)
def enforce_db_rollback_for_all_tests():
yield
db.session.rollback()
def commit_test_data(db_fn):
#functools.wraps(db_fn)
def wrapper():
db_fn()
db.session.commit()
return wrapper
They work quite well until one day I want to add an API test.
def test_admin(app_instance):
test_client = app_instance.test_client()
res = test_client.get("/admin")
# Assert
assert res.status_code == 200
The unit test itself worked fine and passed, however, it broke other unit tests and threw out error like
sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x115a78b90> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/14/bhk3)
I'm trying make some api server with FastAPI.
I have one endpoint named /hello on my project, which gives:
{msg : "Hello World"}
with JSON format when 200 status.
However, It gives error msg when request fails.
Quite simple service. However, I want to test both cases, just for my study. So I also made test code with pytest.
Now I want to know: how can I raise HTTPException and test it on purpose?
#main.py (FAST API)
#app.get('/hello')
def read_main():
try:
return {"msg":"Hello World"}
except requests.exceptions.HTTPError as e:
raise HTTPException(status_code=400,detail='error occured')
#test.py
from fastapi.testclient import TestClient
client = TestClient(app)
# This test works
def test_read_main():
response = client.get("/hello")
assert response.json() == {"msg":"Hello World"}
assert response.status_code == 200
def test_errors():
# How can I test except in endpoint "/hello" ?
# The code below never works as I expect
# with pytest.raises(HTTPException) as e:
# raise client.get("/hello").raise_for_status()
# print(e.value)
The problem here is that your logic is way to simplistic to test. As luk2302 said; in the current form, your except block is never called and thus can never be tested. Replacing your logic with something more testable, allows us to force an Exception being thrown.
File: app.py
from fastapi import FastAPI
from fastapi.exceptions import HTTPException
import requests
app = FastAPI()
#We've put this in a seperate function so we can mock this.
def get_value():
return {"msg":"Hello World"}
#app.get('/hello')
def read_main():
try:
return get_value()
except requests.exceptions.HTTPError as e:
raise HTTPException(status_code=400,detail='error occured')
Note that the return value of your endpoint is now actually provided by the get_value() function.
The test.py file would look like this:
from fastapi import HTTPException
import app
from fastapi.testclient import TestClient
import requests
from pytest_mock import MockerFixture
client = TestClient(app.app)
def test_read_main():
response = client.get("/hello")
assert response.json() == {"msg":"Hello World"}
assert response.status_code == 200
def get_value_raise():
raise requests.exceptions.HTTPError()
def test_errors(mocker: MockerFixture):
mocker.patch("app.get_value", get_value_raise)
response = client.get("/hello")
assert response.status_code == 400
assert response.json() == {"detail": "error occured"}
Note that we replace the app.get_value function with a function that will definitely raise the type of exception that you are catching in your application logic. The response of the test client is (however) just an HTTP response, but with statuscode 400 and a detail in the json body. We assert for that.
The result:
(.venv) jarro#MBP-van-Jarro test_http_exception % pytest test.py
=================================================== test session starts ===================================================
platform darwin -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0
rootdir: /Users/jarro/Development/fastapi-github-issues/SO/test_http_exception
plugins: anyio-3.6.1, mock-3.8.2
collected 2 items
test.py .. [100%]
==================================================== 2 passed in 0.17s ====================================================
I used pytest, and by extension I used pytest-mocker to mock the get_value function.
I have a simple router designed to throw an HTTPException:
#router.get('/404test')
async def test():
raise HTTPException(HTTP_404_NOT_FOUND, "404 test!")
I want to assert that the exception was thrown, as per FastaAPI docs:
def test_test():
response = client.get("/404test")
assert response.status_code == 404
The exception is thrown before the assertion gets evaluated, marking test as failed:
> raise HTTPException(HTTP_404_NOT_FOUND, "404 test!")
E fastapi.exceptions.HTTPException: (404, '404 test!')
What am I missing to properly anticipate HTTPExceptions in my test?
Assuming we have the following route set up in our fastapi app:
#router.get('/404test')
async def test():
raise HTTPException(HTTP_404_NOT_FOUND, "404 test!")
I was able to get a pytest to work with the following code snippet:
from fastapi import HTTPException
def test_test():
with pytest.raises(HTTPException) as err:
client.get("/404test")
assert err.value.status_code == 404
assert err.value.detail == "404 test!"
It seems that the err is the actual HTTPException object, not the json representation. When you catch this error you can then make assertions on that HTTPException object.
Make sure you run the assertions (assert) outside of the with statement block because when the error is raised, it stops all execution within the block after the http call so your test will pass but the assertions will never evaluate.
You can reference the details and the status code and any other attributes on the Exception with err.value.XXX.
May be you can do this using the following sample code.
~/Desktop/fastapi_sample $ cat service.py
from fastapi import FastAPI, HTTPException
app = FastAPI()
#app.get("/wrong")
async def wrong_url():
raise HTTPException(status_code=400, detail="404 test!")
~/Desktop/fastapi_sample $ cat test_service.py
from fastapi.testclient import TestClient
from fastapi_sample.service import app
client = TestClient(app)
def test_read_item_bad_token():
response = client.get("/wrong")
assert response.status_code == 400
assert response.json() == {"detail": "404 test!"}%
~/Desktop/fastapi_sample $ pytest
==================================================================== test session starts ====================================
platform darwin -- Python 3.7.9, pytest-6.1.0, py-1.9.0, pluggy-0.13.1
rootdir: /Users/i869007/Desktop/workspace/SAP/cxai/fastapi_postgres_tutorial
collected 1 item
test_service.py . [100%]
===================================================================== 1 passed in 0.78s ======================================
I am having a problem with making automated tests for flask in python 3. I have tried unittest, pytests, nosetests but I still can't figure out how to form automated tests for flask application.
Following is the code I have wrote using unittest and pytest
unittest:
import unittest
from flaskfolder import flaskapp
class FlaskBookshelfTests(unittest.TestCase):
#classmethod
def setUpClass(cls):
pass
#classmethod
def tearDownClass(cls):
pass
def setUp(self):
# creates a test client
self.flaskapp = flaskapp.test_client()
# propagate the exceptions to the test client
self.flaskapp.testing = True
def tearDown(self):
pass
def test1(self):
result = self.flaskapp.get('/')
self.assertEqual(result.status_code, 200)
In this code i am having error that flaskapp doesn't has any test_client() function.
pytest:
import pytest
from flaskfolder import flaskapp
#pytest.fixture
def client():
db_fd, flaskapp.app.config['DATABASE'] = tempfile.mkstemp()
flaskapp.app.config['TESTING'] = True
with flaskapp.app.test_client() as client:
with flaskapp.app.app_context():
flaskapp.init_db()
yield client
os.close(db_fd)
os.unlink(flaskapp.app.config['DATABASE'])
def test1():
result = client.get('/')
assert result.data in 'Hello World'
In this error that "'function' doesn't has any attribute get" is recieved
and if done: def test1(client) it gives an error that flaskapp doesn't have any attribute init_db.
Because client is a pytest fixture, you need to include it as anargument to your test, so this should solve your current issue
def test1(client):
result = client.get('/')
assert result.data in 'Hello World'