How do I unit test my dependency injection in my Flask app? - python

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

Related

How to pass pytest fixture to the main function without adding it to parameters?

In my project I have a Settings class:
config.py
class Settings(BaseSettings):
DEBUG: bool = os.getenv("DEBUG", 'False')
TOKEN_KEY: str = os.getenv("TOKEN_KEY", '')
TOKEN_PASSWORD: str = os.getenv("TOKEN_PASSWORD", '')
#lru_cache()
def get_settings():
return Settings()
And I'm using it in a method like this:
helpers.py
def generate_new_token(user: User) -> str:
settings = get_settings()
private_key = settings.TOKEN_KEY
token_password = settings.TOKEN_PASSWORD
# Do something
I've created these two fixtures:
conftest.py
#pytest.fixture
def get_settings():
return Settings(DEBUG=True, TOKEN_KEY="SOME_FAKE_TOKEN_KEY", TOKEN_PASSWORD='')
#pytest.fixture
def get_user():
return User()
Now I want to test the generate_new_token method with the values returned from fixtures:
test_helpers.py
def test_generate_new_token(get_user, get_settings):
generate_new_token(user=get_user)
In this case, the TOKEN_KEY value from get_settings should be SOME_FAKE_TOKEN_KEY, but it's still empty.
When I debug the code, I can see that it passes the value from get_settings fixture to test_generate_new_token , but then generate_new_token calls the main get_settings method and does not use the get_settings value from fixture as settings value.
I know that if I pass settings as a parameter to generate_new_token like this:
def generate_new_token(user: DexterUser, settings: Settings) -> str:
I can then pass the fixture to it from test function:
def test_generate_new_token(get_user, get_settings):
generate_new_token(user=get_user, settings=get_settings)
But is there anyway to pass the fixture to the main function without having to add it to its parameters?
Your fixture does not replace the function get_settings, it is just another implementation. What you need to do is to patch the implementation with your own, for example:
conftest.py
from unittest import mock
import pytest
#pytest.fixture
def get_settings():
with mock.patch("your_module.helpers.get_settings") as mocked_settings:
mocked_settings.return_value = Settings(
DEBUG=True,
TOKEN_KEY="SOME_FAKE_TOKEN_KEY",
TOKEN_PASSWORD='')
yield
Note that you have to mock the reference of get_settings that is imported in your helpers module, see where to patch.

pytest create object which is tightly coupling with anther config class?

I need to write tests for the following file.
file1.py
from other import Resource
class _Config:
# ...omit...
#classmethod
def get_config(cls, p1=..., p2=..., p3=..., ...):
return _Config(p1, p2, p3, ..)
def resource(name):
# ....
if name = '...':
return Resource(_Config.get_config())
How to write a pytest fixture for resource?
from ..dir import file1
#pytest.fixture()
def value_resource():
config = file1._Config(....) # how to make the following statement to use config?
return file1.resource('value1')

Global dependency as an argument to each handler

Looking at the docs I got to use my app settings in this way:
import config
...
#router.post('')
async def my_handler(
...
settings: config.SettingsCommon = fastapi.Depends(config.get_settings),
):
...
But I am not satisfied with repeating everywhere import config, config.get_settings.
Is there a way to use settings in my handlers without repeating myself?
Because FastAPI cares about helping you minimize code repetition.
You can use Class Based Views from the fastapi_utils package.
As an example:
from fastapi import APIRouter, Depends, FastAPI
from fastapi_utils.cbv import cbv
from starlette import requests
from logging import Logger
import config
from auth import my_auth
router = APIRouter(
tags=['Settings test'],
dependencies=[Depends(my_auth)] # injected into each query, but my_auth return values are ignored, throw Exceptions
)
#cbv(router)
class MyQueryCBV:
settings: config.SettingsCommon = Depends(config.get_settings) # you can get settings here
def __init__(self, r: requests.Request): # called for each query, after their dependencies have been evaluated
self.logger: Logger = self.settings.logger
self.logger.warning(str(r.headers))
#router.get("/cbv/{test}")
def test_cbv(self, test: str):
self.logger.warning(f"test_cbv: {test}")
return "test_cbv"
#router.get("/cbv2")
def test_cbv2(self):
self.logger.warning(f"test_cbv2")
return "test_cbv2"
It's not currently possible to inject global dependencies. You can still declare them and the code inside the dependencies will run as normal.
Docs on global dependencies for reference.
Without any external dependency, I can think of three ways of using global dependencies. You can set a private variable with your dependency and get that dependency using a function.
You can also use the same approach without a global private variable, but instead using a cache decorator (docs here).
Finally, you can implement the singleton pattern if using a class as a dependency.
Something like:
class Animal:
_singleton = None
#classmethod
def singleton(cls) -> "Animal":
if cls._singleton is None:
cls._singleton = Animal()
return cls._singleton

Pytest fixture with scope “class” doesn't works with "setup_class" method

I'm currently using pytest_addoption to run my API tests, so the tests should run against the environment the user uses on the command line. In my test file, I'm trying to instantiate the UsersSupport class just once, passing the env argument. My code:
conftest.py
import pytest
# Environments
QA1 = 'https://qa1.company.com'
LOCALHOST = 'https://localhost'
def pytest_addoption(parser):
parser.addoption(
'--env',
action='store',
default='qa1'
)
#pytest.fixture(scope='class')
def env(request):
cmd_option = request.config.getoption('env')
if cmd_option == 'qa1':
chosen_env = QA1
elif cmd_option == 'localhost':
chosen_env = LOCALHOST
else:
raise UnboundLocalError('"--env" command line must use "qa1", "localhost"')
return chosen_env
users_support.py
import requests
class UsersSupport:
def __init__(self, env):
self.env = env
self.users_endpoint = '/api/v1/users'
def create_user(self, payload):
response = requests.post(
url=f'{self.env}{self.users_endpoint}',
json=payload,
)
return response
post_create_user_test.py
import pytest
from faker import Faker
from projects import UsersSupport
from projects import users_payload
class TestCreateUser:
#pytest.fixture(autouse=True, scope='class')
def setup_class(self, env):
self.users_support = UsersSupport(env)
self.fake = Faker()
self.create_user_payload = users_payload.create_user_payload
def test_create_user(self):
created_user_res = self.users_support.create_user(
payload=self.create_user_payload
).json()
print(created_user_res)
The issue
When I run pytest projects/tests/post_create_user_test.py --env qa1 I'm getting AttributeError: 'TestCreateUser' object has no attribute 'users_support' error, but if I remove the scope from setup_class method, this method run on every method and not on all methods.
How can I use the env fixture in the setup_class and instantiate the UsersSupport class to use in all methods?
If you use a fixture with class scope, the self parameter does not refer to the class instance. You can, however, still access the class itself by using self.__class__, so you can make class variables from your instance variables.
Your code could look like this:
import pytest
from faker import Faker
from projects import UsersSupport
from projects import users_payload
class TestCreateUser:
#pytest.fixture(autouse=True, scope='class')
def setup_class(self, env):
self.__class__.users_support = UsersSupport(env)
self.__class__.fake = Faker()
self.__class__.create_user_payload = users_payload.create_user_payload
def test_create_user(self):
created_user_res = self.users_support.create_user(
payload=self.create_user_payload
).json() # now you access the class variable
print(created_user_res)
During the test, a new test instance is created for each test.
If you have a default function scoped fixture, it will be called within the same instance of the test, so that the self arguments of the fixture and the current test refer to the same instance.
In the case of a class scoped fixture, the setup code is run in a separate instance before the test instances are created - this instance has to live until the end of all tests to be able to execute teardown code, so it is different to all test instances. As it is still an instance of the same test class, you can store your variables in the test class in this case.

Mock database connection python

I have a file called redis_db.py which has code to connect to redis
import os
import redis
import sys
class Database:
def __init__(self, zset_name):
redis_host = os.environ.get('REDIS_HOST', '127.0.0.1')
redis_port = os.environ.get('REDIS_PORT', 6379)
self.db = redis.StrictRedis(host=redis_host, port=redis_port)
self.zset_name = zset_name
def add(self, key):
try:
self.db.zadd(self.zset_name, {key: 0})
except redis.exceptions.ConnectionError:
print("Unable to connect to redis host.")
sys.exit(0)
I have another file called app.py which is like this
from flask import Flask
from redis_db import Database
app = Flask(__name__)
db = Database('zset')
#app.route('/add_word/word=<word>')
def add_word(word):
db.add(word)
return ("{} added".format(word))
if __name__ == '__main__':
app.run(host='0.0.0.0', port='8080')
Now I am writing unit test for add_word function like this
import unittest
import sys
import os
from unittest import mock
sys.path.append(os.path.dirname(os.path.realpath(__file__)) + "/../api/")
from api import app # noqa: E402
class Testing(unittest.TestCase):
def test_add_word(self):
with mock.patch('app.Database') as mockdb:
mockdb.return_value.add.return_value = ""
result = app.add_word('shivam')
self.assertEqual(result, 'shivam word added.')
Issue I am facing is that even though I am mocking the db method call it is still calling the actual method in the class instead of returning mocked values and during testing I am getting error with message Unable to connect to redis host..
Can someone please help me in figuring out how can I mock the redis database calls.
I am using unittest module
The issue is that db is defined on module import, so the mock.patch does not affect the db variable. Either you move the instantiation of
db in the add_word(word) function or you patch db instead of Database, e.g.
def test_add_word():
with mock.patch('api.app.db') as mockdb:
mockdb.add = mock.MagicMock(return_value="your desired return value")
result = app.add_word('shivam')
print(result)
Note that the call of add_word has to be in the with block, otherwise the unmocked version is used.

Categories