Testing flask_wtf/wtforms with pytest - python-3.x

I'd like to test a POST route that processes a non-trivial form (by working with flask.request.form). I didn't really find a good tutorial for this somehow as most pass json data rather than form (or is it the same?).
I tried to write the code in the following way:
import pytest
import app #app.app is the Flask app
#pytest.fixture
def client():
app.app.config['TESTING'] = True
with app.app.test_client() as client:
with app.app.app_context():
yield client
def test_route_webapp_post(client):
form = app.forms.ImputeForm.make_form(data_dict=app.data_dictionary.data_dict,
numeric_fields=app.binaries_dict['numeric_mappers'].keys(),
recordname2description=app.binaries_dict['recordname2description'])
rv = client.post('/web_app',form=form)
assert rv.status_code==200
The form is generated dynamically and I don't always know ahead of time what are the fields:
from flask_wtf import FlaskForm
from wtforms import SelectField, DecimalField, BooleanField
class ImputeForm(FlaskForm):
#classmethod
def make_form(cls, data_dict, numeric_fields, recordname2description, request_form=None):
for key in numeric_fields:
setattr(cls, key, DecimalField(id=key, label=recordname2description[key].split('(')[0]))
setattr(cls, 'mask_' + key, BooleanField(label='mask_' + key))
for key in data_dict:
setattr(cls, key, SelectField(id=key, label=recordname2description[key],
choices=[(-1, 'None selected')]+list(data_dict[key].items())))
setattr(cls, 'mask_' + key, BooleanField(label='mask_' + key))
instance = cls(request_form)
return instance
But this doesn't really work as I can't make a form inside the test case and get
E RuntimeError: Working outside of request context.
E
E This typically means that you attempted to use functionality that needed
E an active HTTP request. Consult the documentation on testing for
E information about how to avoid this problem.
So what is the proper approach to testing my form (in particular I am ok with sending an empty one)?

The correct way is to create a python dictionary and pass it as "data", not to try to create a form.
In particular case this involved making a new function:
def make_from_data( data_dict, numeric_fields):
data = dict()
for key in numeric_fields:
data[key]='234'
data['mask_' + key]='y'
for key in data_dict:
data[key]=-1
data['mask_' + key]='y'
return data
and passing it as follows:
def test_route_webapp_post(client):
data = make_from_data(data_dict=app.data_dictionary.data_dict,
numeric_fields=app.binaries_dict['numeric_mappers'].keys())
rv = client.post('/web_app',data=data)
assert rv.status_code==200

Related

PyTest how to properly mock imported ContextManager class and its function?

This is my sample code:
from path.lib import DBInterface
class MyClass:
def __init__(self):
self.something = "something"
def _my_method(self, some_key, new_setup):
with DBInterface(self.something) as ic:
current_setup = ic.get(some_key)
if current_setup != new_setup:
with DBInterface(self.something) as ic:
ic.set(new_setup)
def public_method(self, some_key, new_setup):
return self._my_method(some_key, new_setup)
(my actual code is bit more complex, but i cant put it here on public :)
Now, what I want to do is, I want to completely mock the imported class DBInterface, because I do not want my unittests to do anything in DB.
BUT I also need the ic.get(some_key) to return some value, or to be more precise, I need to set the value it returns, because thats the point of my unittests, to test if the method behave properly according to value returned from DB.
This is how far I got:
class TestMyClass:
def test_extractor_register(self, mocker):
fake_db = mocker.patch.object('my_path.my_lib.DBInterface')
fake_db.get.return_value = None
# spy_obj = mocker.spy(MyClass, "_my_method")
test_class = MyClass()
# Test new registration in _extractor_register
result = test_class.public_method(Tconf.test_key, Tconf.test_key_setup)
fake_db.assert_has_calls([call().__enter__().get(Tconf.test_key),
call().__enter__().set(Tconf.test_key, Tconf.test_key_setup)])
# spy_obj.assert_called_with(ANY, Tconf.test_key, Tconf.test_key_setup)
assert result.result_status.status_code == Tconf.status_ok.status_code
assert result.result_data == MyMethodResult.new_reg
But i am unable to set return value for call().__enter__().get(Tconf.test_key).
I have been trying many approaches:
fake_db.get.return_value = None
fake_db.__enter__().get.return_value = None
fake_db.__enter__.get = Mock(return_value=None)
mocker.patch.object(MyClass.DBInterface, "get").return_value = None
None of that is actually working and I am running out of options I can think about.
Without having more code or errors that are being produced, it's tough to provide a conclusive answer.
However, if you truly only need to specify a return value for set() I would recommend using MagicMock by virtue of patch --
from unittest.mock import patch
#patch("<MyClassFile>.DBInterface", autospec=True)
def test_extractor_register(mock_db):
mock_db.set.return_value = "some key"
# Rest of test code

How do I use a pytest fixture to mock a child class's inherited methods with classes as properties while maintaining the API contract using autospec?

How it started
I'm testing a class, ClassToTest, that makes API calls using atlassian-python-api. The tests are going to ensure that ClassToTest performs correctly with the data it gets back from the API. Many of the atlassian-python-api API calls use instantiated classes which inherit from the same base class or group of top-level classes.
I'd like to write tests that will expose breaks in the API contract if the wrong data is returned or API calls fail, while also testing the class I wrote to ensure it does the correct things with the data returned from the API. In order to do this, I was hoping to use unittest.mock.patch("path.to.Comment", autospec=True) to copy the API spec into the MagicMock, but I don't believe it's working properly.
For the purposes of the question, ClassToTest is not that important; what I am aiming to solve is how to setup and configure the pytest fixtures in a way that I can use them to mimic the API endpoints that will return the data that ClassToTest will act upon. Ideally I'd like to reuse the fixtures without having patch conflicts. I've included relevant code from ClassToTest for illustrative purposes here:
class_to_test.py:
from atlassian.bitbucket import Cloud
from typing import NamedTuple
# these are hardcoded constants that work with the production API
from src.constants import (
PULL_REQUEST_ID,
REPOSITORY,
WORKSPACE,
)
CommentType = NamedTuple("CommentType", [("top_level", str), ("inline", str)])
class ClassToTest:
def _get_token(self):
"""this returns a token of type(str)"""
def __init__(self, workspace, repository, pull_request_id):
self.active_comments = None
self.environment = sys.argv[1]
self.comment_text = CommentType(
top_level=r"top_level_comment text", inline=r"inline_comment text"
)
self.cloud = Cloud(token=self._get_token(), cloud=True)
self.workspace = self.cloud.workspaces.get(workspace)
self.repository = self.cloud.repositories.get(workspace, repository)
self.pull_request = self.repository.pullrequests.get(id=pull_request_id)
def _get_active_comments(self):
"""Returns a list of active (non-deleted) comments"""
return [
c for c in self.pull_request.comments() if c.data["deleted"] is False
]
# a few more methods here
def main():
instance = ClassToTest(WORKSPACE, REPOSITORY, PULL_REQUEST_ID)
# result = instance.method() for each method I need to call.
# do things with each result
if __name__ == "__main__":
main()
The class has methods that retrieve comments from the API (_get_active_comments, above), act on the retrieved comments, retrieve pull requests, and so on. What I am trying to test is that the class methods act correctly on the data received from the API, so I need to accurately mock data returned from API calls.
How it's going
I started with a unittest.Testcase style test class and wanted the flexibility of pytest fixtures (and autospec), but removed Testcase entirely when I discovered that pytest fixtures don't really work with it. I'm currently using a pytest class and conftest.py as follows:
/test/test_class_to_test.py:
import pytest
from unittest.mock import patch
from src.class_to_test import ClassToTest
#pytest.mark.usefixtures("mocked_comment", "mocked_user")
class TestClassToTest:
# We mock Cloud here as ClassToTest calls it in __init__ to authenticate with the API
# _get_token retrieves an access token for the API; since we don't need it, we can mock it
#patch("src.test_class_to_test.Cloud", autospec=True)
#patch.object(ClassToTest, "_get_token").
def setup_method(self, method, mock_get_token, mock_cloud):
mock_get_token.return_value = "token"
self.checker = ClassToTest("WORKSPACE", "REPOSITORY", 1)
def teardown_method(self, method):
pass
def test_has_top_level_and_inline_comments(self, mocked_comment, mocked_pull_request):
mock_top_comment = mocked_comment(raw="some text to search for later")
assert isinstance(mock_top_comment.data, dict)
assert mock_top_comment.data["raw"] == "some text to search for later"
# the assert below this line is failing
assert mock_top_comment.user.account_id == 1234
conftest.py:
import pytest
from unittest.mock import patch, PropertyMock
from atlassian.bitbucket.cloud.common.comments import Comment
from atlassian.bitbucket.cloud.common.users import User
#pytest.fixture()
def mocked_user(request):
def _mocked_user(account_id=1234):
user_patcher = patch(
f"atlassian.bitbucket.cloud.common.users.User", spec_set=True, autospec=True
)
MockUser = user_patcher.start()
data = {"type": "user", "account_id": account_id}
url = "user_url"
user = MockUser(data=data, url=url)
# setup mocked properties
mock_id = PropertyMock(return_value=account_id)
type(user).id = mock_id
mockdata = PropertyMock(return_value=data)
type(user).data = mockdata
request.addfinalizer(user_patcher.stop)
return user
return _mocked_user
#pytest.fixture()
def mocked_comment(request, mocked_user):
def _mocked_comment(raw="", inline=None, deleted=False, user_id=1234):
comment_patcher = patch(
f"atlassian.bitbucket.cloud.common.comments.Comment", spec_set=True, autospec=True
)
MockComment = comment_patcher.start()
data = {
"type": "pullrequest_comment",
"user": mocked_user(user_id),
"raw": raw,
"deleted": deleted,
}
if inline:
data["inline"] = {"from": None, "to": 1, "path": "src/code_issues.py"}
data["raw"] = "this is an inline comment"
comment = MockComment(data)
# setup mocked properties
mockdata = PropertyMock(return_value=data)
type(comment).data = mockdata
# mockuser = PropertyMock(return_value=mocked_user(user_id))
# type(comment).user = mockuser
request.addfinalizer(comment_patcher.stop)
return comment
return _mocked_comment
The problem I am encountering is that the assert mock_top_comment.user.account_id == 1234 line fails when running the test, with the following error:
> assert mock_top_comment.user.account_id == 1234
E AssertionError: assert <MagicMock name='Comment().user.account_id' id='4399290192'> == 1234
E + where <MagicMock name='Comment().user.account_id' id='4399290192'> = <MagicMock name='Comment().user' id='4399634736'>.account_id
E + where <MagicMock name='Comment().user' id='4399634736'> = <NonCallableMagicMock name='Comment()' spec_set='Comment' id='4399234928'>.user
How do I get the mock User class to attach to the mock Comment class in the same way that the real API makes it work? Is there something about autospec that I'm missing, or should I be abandoning unittest.mock.patch entirely and using something else?
Extra credit (EDIT: in retrospect, this may be the most important part)
I'm using mocked_comment as a pytest fixture factory and want to reuse it multiple times in the same test (for example to create multiple mocked Comments returned in a list). So far, each time I've tried to do that, I've been met with the following error:
def test_has_top_level_and_inline_comments(self, mocked_comment, mocked_pull_request):
mock_top_comment = mocked_comment(raw="Some comment text")
> mock_inline_comment = mocked_comment(inline=True)
...
test/conftest.py:30: in _mocked_comment
MockComment = comment_patcher.start()
/opt/homebrew/Cellar/python#3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10/unittest/mock.py:1585: in start
result = self.__enter__()
...
> raise InvalidSpecError(
f'Cannot autospec attr {self.attribute!r} from target '
f'{target_name!r} as it has already been mocked out. '
f'[target={self.target!r}, attr={autospec!r}]')
E unittest.mock.InvalidSpecError: Cannot autospec attr 'Comment' from target 'atlassian.bitbucket.cloud.common.comments' as it has already been mocked out. [target=<module 'atlassian.bitbucket.cloud.common.comments' from '/opt/homebrew/lib/python3.10/site-packages/atlassian/bitbucket/cloud/common/comments.py'>, attr=<MagicMock name='Comment' spec_set='Comment' id='4398964912'>]
I thought the whole point of a pytest fixture factory was to be reusable, but I believe that using an autospec mock complicates things quite a bit. I don't want to have to hand copy every detail from the API spec into the tests, as that will have to be changed if anything in the API changes. Is there a solution for this that involves automatically and dynamically creating the necessary classes in the mocked API with the correct return values for properties?
One thing I'm considering is separating the testing into two parts: API contract, and ClassToTest testing. In this way I can write the tests for ClassToTest without relying on the API and they will pass as long as I manipulate the received data correctly. Any changes to the API will get caught by the separate contract testing tests. Then I can use non-factory fixtures with static data for testing ClassToTest.
For now though, I'm out of ideas on how to proceed with this. What should I do here? Probably the most important thing to address is how to properly link the User instance with the Comment instance in the fixtures so that my method calls in test work the same way as they do in production. Bonus points if we can figure out how to dynamically patch multiple fixtures in a single test.
I've started looking at this answer, but given the number of interconnected classes and properties, I'm not sure it will work without writing out a ton of fixtures. After following the directions and applying them to the User mock inside the Comment mock, I started getting the error in the Extra Credit section above, where autospec couldn't be used as it has already been mocked out.

Python mocking using MOTO for SSM

Taken from this answer:
Python mock AWS SSM
I now have this code:
test_2.py
from unittest import TestCase
import boto3
import pytest
from moto import mock_ssm
#pytest.yield_fixture
def s3ssm():
with mock_ssm():
ssm = boto3.client("ssm")
yield ssm
#mock_ssm
class MyTest(TestCase):
def setUp(self):
ssm = boto3.client("ssm")
ssm.put_parameter(
Name="/mypath/password",
Description="A test parameter",
Value="this is it!",
Type="SecureString",
)
def test_param_getting(self):
import real_code
resp = real_code.get_variable("/mypath/password")
assert resp["Parameter"]["Value"] == "this is it!"
and this is my code to test (or a cut down example):
real_code.py
import boto3
class ParamTest:
def __init__(self) -> None:
self.client = boto3.client("ssm")
pass
def get_parameters(self, param_name):
print(self.client.describe_parameters())
return self.client.get_parameters_by_path(Path=param_name)
def get_variable(param_name):
p = ParamTest()
param_details = p.get_parameters(param_name)
return param_details
I have tried a number of solutions, and switched between pytest and unittest quite a few times!
Each time I run the code, it doesn't reach out to AWS so it seems something is affecting the boto3 client, but it doesn't return the parameter. If I edit real_code.py to not have a class inside it the test passes.
Is it not possible to patch the client inside the class in the real_code.py file? I'm trying to do this without editing the real_code.py file at all if possible.
Thanks,
The get_parameters_by_path returns all parameters that are prefixed with the supplied path.
When providing /mypath, it would return /mypath/password.
But when providing /mypath/password, as in your example, it will only return parameters that look like this: /mypath/password/..
If you are only looking to retrieve a single parameter, the get_parameter call would be more suitable:
class ParamTest:
def __init__(self) -> None:
self.client = boto3.client("ssm")
pass
def get_parameters(self, param_name):
# Decrypt the value, as it is stored as a SecureString
return self.client.get_parameter(Name=param_name, WithDecryption=True)
Edit: Note that Moto behaves the same as AWS in this.
From https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameters_by_path:
[The Path-parameter is t]he hierarchy for the parameter. [...] The hierachy is the parameter name except the last part of the parameter. For the API call to succeeed, the last part of the parameter name can't be in the path.

How to convert python dict to DictRow object

Hi I am writing unittest using pytest. But I am not able to mock few db functions. We are using psycopg2 for db connections and executions. Response of query returned from psycopg2 is of the type DictRow which can be accessed either by key or by index.
Ex:
response = ['prajwal', '23', 'engineer'] #Response of a query "select name, age , job from users"
>>>response[0]
'prajwal'
>>>response['name']
'prajwal'
I want to know is there any way by which we can covert dict/list to above mentioned type.
Looking at the source for psycopg2, creating a DictRow requires passing in a DictCursor object. However the only thing it uses from DictCursor appears to be an index and description attribute.
# found in lib\site-packages\psycopg2.extras.py
class DictRow(list):
"""A row object that allow by-column-name access to data."""
__slots__ = ('_index',)
def __init__(self, cursor):
self._index = cursor.index
self[:] = [None] * len(cursor.description)
The index looks like a dict with a mapping a key to an index. e.g.response['name'] = 0
The description looks like your dict that you want to convert.
If you're feeling hacky you could take advantage of duck typing and pretend you're passing in a cursor when you're just satisfying the requirements.
The only caveat is after we instantiate the DictRow, we need to populate it. Our fake cursor hack will take care of the rest.
from psycopg2.extras import DictRow
class DictRowHack:
def __init__(self, my_dict):
# we need to set these 2 attributes so that
# it auto populates our indexes
self.index = {key: i for i, key in enumerate(my_dict)}
self.description = my_dict
def dictrow_from_dict(my_dict):
# this is just a little helper function
# so you don't always need to go through
# the steps to recreate a DictRow
fake_cursor = DictRowHack(my_dict)
my_dictrow = DictRow(fake_cursor)
for k, v in my_dict.items():
my_dictrow[k] = v
return my_dictrow
response = {'name': 'prajwal', 'age': '23', 'job': 'engineer'}
my_dictrow = dictrow_from_dict(response)
print(my_dictrow[1])
print(my_dictrow['name'])
print(type(my_dictrow))

How to store in variable function returning value (kivy properties)

class Data(object):
def get_key_nicks(self):
'''
It returns key and nicks object
'''
file = open(self.key_address, 'rb')
key = pickle.load(file)
file.close()
file = open(self.nicks_address, 'rb')
nicks = pickle.load(file)
file.close()
return (key, nicks)
Above is the data api and function which i want to use in kivy
class MainScreen(FloatLayout):
data = ObjectProperty(Data())
key, nicks = ListProperty(data.get_key_nicks())
it gives error like: AttributeError: 'kivy.properties.ObjectProperty' object has no attribute 'get_key_nicks'
Properties are descriptors, which basically means they look like normal attributes when accessed from instances of the class, but at class level they are objects on their own. That's the nature of the problem here - at class level data is an ObjectProperty, even though if you access it from an instance of the class you'll get your Data() object that you passed in as the default value.
That said, I don't know what your code is actually trying to do, do you want key and nicks to be separate ListProperties?
Could you expand a bit more on what you're trying to do?
I think all you actually need to do is:
class MainScreen(FloatLayout):
data = ObjectProperty(Data())
def get_key_nicks(self):
return data.get_key_nicks()

Resources