I have this configuration (for demonstration purposes)
endpoints.py
celery_conf.py
Inside celery client is the configuration setup for celery, and inside endpoints.py there is for example an endpoint where celery_client is imported.
In endpoints.py I import celery_client (instantiated Celery() object)
#in endpoints.py
from celery_conf import celery_client
#router.post(
include_in_schema=True,
status_code=status.HTTP_200_OK,
name="some_name:post"
)
def foo_endpoint(
item: PydanticModel, db: Session = Depends(get_database)
) -> dict:
tmp = <some preprocessing of item>
celery_client.send_task(...)
return 200
I want to test this endpoint and see if celery_client.send_task() has been invoked.
How can i do this? I have read about pytest patch feature, but I do not understand how to test it.
Lets say I have this test:
client = TestClient() #fastapi test client
def test_enpoint():
#patch where celery client is imported
with patch('endpoints.celery_client') as mock_task:
client.put(url=app.url_path_for("some_name:post"), data={})
...
How do I test if celery_client.send_task() has been ivoked inside endpoint?
You can do this with:
with patch("endpoints.celery_client.send_task") as mock_task:
client.put(url=app.url_path_for("some_name:post"), data={})
assert mock_task.call_count == 1
assert mock_task.call_args
or there is also the pytest-mock package that can help:
def test_endpoint(mocker: MockerFixture):
mock_task = mocker.patch("endpoints.celery_client.send_task")
client.put(url=app.url_path_for("some_name:post"), data={})
mock_task.assert_called_once()
mock_task.assert_called_once_with(arg1, arg2)
Related
I've been following those tutorials on (unit) testing for dependency injections and they're using pytest fixtures and I'm trying to replicate something similar in my Flask app. This is what my app looks like:
# all imports
class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(modules=[".routes", ".scheduler"])
config = providers.Configuration(yaml_files=["src/conf/config.yaml"])
config.load(envs_required=True)
s3_repository = providers.Resource(
S3Repository, config.get("app.my_service.s3_bucket")
)
my_service = providers.Singleton(
MyService, config, s3_repository
)
My app.py:
container = Container()
container.init_resources()
app = Flask(__name__)
app.container = container
# connect url rules and register error handlers
routes.configure(app)
# schedule and kickoff background jobs
scheduler.schedule(app)
# set flask configuration and logging
app.config.from_mapping(app.container.config.get("app"))
setup_logging(app)
return app
my_service.py
class MyService:
def __init__(self, config: dict, s3_repository: S3Repository) -> None:
self.s3 = s3_repository
self.config = config
# other logic/methods
My S3Repository:
class S3Repository:
def __init__(self, bucket):
self.bucket = bucket
def fetch(self, object_key, columns, filters):
# code to fetch
I'm trying to write my tests and working with pytest for the first time and this is what I have so far:
# TODO - re-write tests for since we're now using dependency injection
import unittest
from unittest.mock import Mock
import pytest as pytest
from src.repository.s3_repository import S3Repository
from src.service.HealthSignalService import HealthSignalService
class TestApp(unittest.TestCase):
def something(self):
pass
#pytest.fixture
def mock_config(mocker):
return mocker.patch("providers.Configuration")
def test_app(mock_config):
from src import create_app
create_app()
When I run this I see:
#pytest.fixture
def mock_config(mocker):
E fixture 'mocker' not found
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, mock_config, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
> use 'pytest --fixtures [testpath]' for help on them.
What am I doing wrong? What am I missing? Right now, I'm following this tutorial - https://docs.pytest.org/en/7.1.x/explanation/fixtures.html
You never define a pytest.fixture with the name of mocker. The arguments passing to the pytest function arguments must be defined by pytest.fixture
for example
#pytest.fixture
def connection():
...
#pytest.fixture
def database(connection):
...
def test_somecase(connection, database):
...
those arguments already defined by the pytest.fixture
I am quite new to unit testing and relatively new to RESTful API development as well. I am wondering how to do unit test for functions inside Resource class in flask restful? I can do unit test for the endpoint's response but I don't know how to do testing for the individual functions inside the endpoint's controller class.
Below is my application code. It has 3 files including test:
api.py
controller_foo.py
test_controller_foo.py
# api.py
from flask import Flask
from flask_restful import Api
from .controller_foo import ControllerFoo
def create_app(config=None):
app = Flask(__name__)
app.config['ENV'] ='development'
return app
application = app = create_app()
api = Api(app)
api.add_resource(ControllerFoo, '/ctrl')
if __name__ == "__main__":
app.run(debug=True)
# controller_foo.py
from flask_restful import Resource
from flask import request
class ControllerFoo(Resource):
"""
basically flask-restful's Resource method is a wrapper for flask's MethodView
"""
def post(self):
request_data = self.handle_request()
response = self.process_request(request_data)
return response
def handle_request(self):
json = request.get_json()
return json
def process_request(self, data):
# do some stuffs here
return {'foo': 'bar'}
I am using unittest
# test_controller_foo.py
import unittest
from api import app
from .controller_foo import ControllerFoo
# initiating class to try testing but I don't know how to start
ctrl = ControllerFoo()
class ControllerFooTestCase(unittest.TestCase):
def setUp(self):
self.app = app
self.app.config['TESTING'] = True
self.client = app.test_client()
self.payload = {'its': 'empty'}
def tearDown(self):
pass
def test_get_response(self):
response = self.client.post('/ctrl', json=self.payload)
expected_resp = {
'foo': 'bar'
}
self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.get_json(), expected_resp)
if __name__ == "__main__":
unittest.main()
I want to know how to properly do unit test for handle_request and process_request function
EDIT: Fixing out my buggy code. Thanks Laurent LAPORTE for the highlights.
There are several bugs in your code, so this is not easy to explain.
First of all, the recommended way to do testing with Flask (and Flask-Restful) is to use PyTest instead of unittest, because it is easier to setup and use.
Take a look at the documentation: Testing Flask Applications.
But, you can start with unittest…
note: you can have a confusion with your app module and the app instance in that module. So, to avoid it, I imported the module. Another good practice is to name your test module against the tested module: "app.py" => "test_app.py". You can also have a confusion with the controller module and the controller instance. The best practice is to use a more precise name, like "controller_foo" or something else…
Here is a working unit test:
# test_app.py
import unittest
import app
class ControllerTestCase(unittest.TestCase):
def setUp(self):
self.app = app.app
self.app.config['TESTING'] = True
self.client = self.app.test_client()
self.payload = {'its': 'empty'}
def test_get_response(self):
response = self.client.post('/ctrl', json=self.payload)
expected_resp = {'foo': 'bar'}
self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.get_json(), expected_resp)
if __name__ == "__main__":
unittest.main()
As you can see, I also fixed the posted URL, in your application, the URL is "/ctrl", not "controller".
At this point, the test can run, but you have another error:
Ran 1 test in 0.006s
FAILED (errors=1)
Error
Traceback (most recent call last):
...
TypeError: process_request() takes 1 positional argument but 2 were given
If you take a look at your process_request() method, you can see that you missed the self parameter. Change it like this.
def process_request(self, data):
# do some stuffs here
return {'foo': 'bar'}
Your test should pass.
But, that not the right way to implement Flask-Restful controolers. Read the doc and use get and post methods…
I have the following structure for the project
myapp
|
-contrib
|
- helpers.py
- views
|
- app_view.py
In helpers.py I have the following method
def method(**kwargs) -> int:
# Some code here
requests.post(
url=f"{REMOTE_SERVER}/sc",
json=kwargs,
timeout=5
)
In app_view.py I have the following implementation. I am calling the method inside a thread and I need to mock it using pytest to check whether it's being called once and called with certain number of parameters.
import myapp.contrib.helpers import method
class Score(MethodView):
#prepare_req
def post(self, **kwargs):
"""
Post Score
"""
response_data = dict()
# Some logic here
self.filter_score(kwargs)
return jsonify(response_data)
def filter_score(self, **kwargs)
thread = threading.Thread(target=method, kwargs=event_data)
thread.start()
Using pytest I am trying to mock the method() as follows.
def test_score(client, mocker):
url = "sc/1.1/score"
patched_method = mocker.patch(
"myapp.contrib.helpers.method"
)
client.post(url, json=issue_voucher_post_data)
patched_method.assert_called_once() # <- this is getting false and not mocking the method
Tried the below as well but it wont patch properly
patched_method = mocker.patch(
"myapp.views.app_view.method"
)
But it will call the corresponding view
How to mock a function used inside a thread in a view which is available in another module using pytest
?
Is it possible to write pytest fixtures as tornado coroutines? For example, I want to write a fixture for creating a db, like this:
from tornado import gen
import pytest
#pytest.fixture
#gen.coroutine
def get_db_connection():
# set up
db_name = yield create_db()
connection = yield connect_to_db(db_name)
yield connection
# tear down
yield drop_db(db_name)
#pytest.mark.gen_test
def test_something(get_db_connection):
# some tests
Obviously, this fixture does not work as expected, as it is called as a function, not as coroutine. Is there a way to fix it?
After some research, I came out with this solution:
from functools import partial
from tornado import gen
import pytest
#pytest.fixture
def get_db_connection(request, io_loop): # io_loop is a fixture from pytest-tornado
def fabric():
#gen.coroutine
def set_up():
db_name = yield create_db()
connection = yield connect_to_db(db_name)
raise gen.Return(connection)
#gen.coroutine
def tear_down():
yield drop_db(db_name)
request.addfinalizer(partial(io_loop.run_sync, tear_down))
connection = io_loop.run_sync(set_up)
return connection
return fabric
#pytest.mark.gen_test
def test_something(get_db_connection):
connection = get_db_connection() # note brackets
I'm sure, that it can be done cleaner using some pylint magic.
I found this slides very useful.
EDIT: I found out that above method has a limitation. You can't change scope of get_db_connection fixture, as it uses io_loop fixture with scope "function".
I'm trying to patch an API for unit testing purposes. I have a class like this:
# project/lib/twilioclient.py
from os import environ
from django.conf import settings
from twilio.rest import TwilioRestClient
def __init__(self):
return super(Client, self).__init__(
settings.TWILIO_ACCOUNT_SID,
settings.TWILIO_AUTH_TOKEN,
)
client = Client()
and this method:
# project/apps/phone/models.py
from project.lib.twilioclient import Client
# snip imports
class ClientCall(models.Model):
'call from Twilio'
# snip model definition
def dequeue(self, url, method='GET'):
'''"dequeue" this call (take off hold)'''
site = Site.objects.get_current()
Client().calls.route(
sid=self.sid,
method=method,
url=urljoin('http://' + site.domain, url)
)
and then the following test:
# project/apps/phone/tests/models_tests.py
class ClientCallTests(BaseTest):
#patch('project.apps.phone.models.Client', autospec=True)
def test_dequeued(self, mock_client):
cc = ClientCall()
cc.dequeue('test', 'TEST')
mock_client.calls.route.assert_called_with(
sid=cc.sid,
method='TEST',
url='http://example.com/test',
)
When I run the test, it fails because the real client is called instead of the mock client.
Looks like it should work to me, and I've been able to make this type of thing work before. I've tried the usual suspects (removing *.pyc) and things don't seem to be changing. Is there a new behavior here that I'm missing somehow?