I have a very simple fastapi application which i want to test , the code for dummy_api.py is as follows :
import os
from fastapi import FastAPI
app = FastAPI()
#app.get(os.getenv("ENDPOINT", "/get"))
def func():
return {
"message": "Endpoint working !!!"
}
When i want to test this i am using the below file :
from fastapi.testclient import TestClient
import dummy_api
def test_dummy_api():
client = TestClient(dummy_api.app)
response = client.get("/get")
assert response.status_code == 200
def test_dummy_api_with_envar(monkeypatch):
monkeypatch.setenv("ENDPOINT", "dummy")
client = TestClient(dummy_api.app)
response = client.get("/dummy")
assert response.status_code == 200
However i am unable to mock the environment variable part as one of the tests fail with a 404.
pytest -s -v
================================================================= test session starts ==================================================================
platform linux -- Python 3.8.5, pytest-6.2.2, py-1.9.0, pluggy-0.13.1 -- /home/subhayan/anaconda3/envs/fastapi/bin/python
cachedir: .pytest_cache
rootdir: /home/subhayan/Codes/ai4bd/roughdir
collected 2 items
test_dummy_api.py::test_dummy_api PASSED
test_dummy_api.py::test_dummy_api_with_envar FAILED
======================================================================= FAILURES =======================================================================
______________________________________________________________ test_dummy_api_with_envar _______________________________________________________________
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7ff8c4cf1430>
def test_dummy_api_with_envar(monkeypatch):
monkeypatch.setenv("ENDPOINT", "dummy")
client = TestClient(dummy_api.app)
response = client.get("/dummy")
> assert response.status_code == 200
E assert 404 == 200
E +404
E -200
test_dummy_api.py:15: AssertionError
=============================================================== short test summary info ================================================================
FAILED test_dummy_api.py::test_dummy_api_with_envar - assert 404 == 200
============================================================= 1 failed, 1 passed in 0.19s ==============================================================
Can anyone point out where am i going wrong please !!
You could use parametrized fixtures and the importlib.reload function to test that environment variable is indeed used.
My test directory looks like this:
.
└── tests
├── conftest.py
├── dummy_api.py
└── test_api.py
Here is my conftest.py:
import pytest
from fastapi.testclient import TestClient
from importlib import reload
import dummy_api
#pytest.fixture(params=["/get", "/dummy", "/other"])
def endpoint(request, monkeypatch):
monkeypatch.setenv("ENDPOINT", request.param)
return request.param
#pytest.fixture()
def client(endpoint):
app = reload(dummy_api).app
yield TestClient(app=app)
And here is the test_api.py file:
import os
def test_dummy_api(client):
endpoint = os.environ["ENDPOINT"]
response = client.get(endpoint)
assert response.status_code == 200
assert response.json() == {"message": f"Endpoint {endpoint} working !"}
Test output after running pytest:
collected 3 items
tests/test_api.py::test_dummy_api[/get] PASSED [ 33%]
tests/test_api.py::test_dummy_api[/dummy] PASSED [ 66%]
tests/test_api.py::test_dummy_api[/other] PASSED [100%]
To me it looks like this:
When the test file is loaded by pytest then all things that can be executed in dummy_api will be executed because it is imported.
This means that the decorator of the decorated function (i.e. #app.get(...)) will be executed. At this point in time monkey-patching has not kicked in.
Once the test function kicks in, the environment variable has been set too late.
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 ======================================================
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'
def setup_method(usern,pwd):
global token,session
inputdata=''
session = requests.Session()
inputdata={
"username":"XXXXXt",
"password":"<XXXxx"
}
response = session.post(config.login_url,data=inputdata,headers=config.api_headers)
token = json.loads(response.text).get('token')
config.api_headers["X-CSRF-Token"]=json.loads(response.text).get('token')
def teardown_method():
inputdata=''
config.api_headers["X-CSRF-Token"]=token
session.post(config.logout_url,data=inputdata,headers=config.api_headers)
#print("logout:",token)
#assert (json.loads(response.text)).get('ResponseStatus') in "SUCCESS"
How to handle session which is generated in the setup and teardown methods in pytest in another file test_1.py?
Please use conftest.py for the same - please refer to this documentation available at https://docs.pytest.org/en/2.7.3/plugins.html?highlight=re