I am writing unit test cases for my fastapi project and unable to mock a dynamodb call.
File_1
This file has all the methods to perform DynamoDB actions using boto3 calls.
class DynamoDBRepository:
Insert Item - Inserts value
Get Item - Returns value
#File_2
Has a "AppConfig" class which will be used as a dependency in a later file
from file_1 import DynamoDBRepository
class AppConfig:
def __init__(self) -> None:
"""Constructor class to instantiate dynamodb"""
self._job_table = "Dynamo_DB_Table"
self._region = "Table_region"
self._dynamodb_repository = DynamoDBRepository(table=self._job_table, region=self._region) # created a object for the dynamodb class mentioned in file 1.
File_3:
This file has the fast_api route decorator
from file_2 import AppConfig
#router.get(
"/object/{object_id}"
)
def get_request(
object_id: str,
objects: AppConfig = Depends(AppConfig),
) -> ObjectBody:
try:
object_detail = objects._dynamodb_repository.get_item({"object_id": object_id})
return object_detail["Item"]
I am trying to mock the get_item method in my test file:
File_4
This is my test file in which
client = TestClient(fast_api_app)
class MockAppConfig:
def __init__(self) -> None:
"""Constructor class to instantiate dynamodb and lambda"""
self._job_table = "Dynamo_DB_Table"
self._region = "Table_region"
self._dynamodb_repository = DynamoDBRepository(table=self._job_table, region=self._region)
def test_get_request():
fast_api_app.dependency_overrides[AppConfig] = MockAppConfig
MockAppConfig()._dynamodb_repository.get_item = {
"id": "1234",
"title": "Foo",
"description": "Hello",
}
response = client.get("/objects/1234")
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "Hi",
}
The mocking of get_item is not working and it is still querying the original db and failing due to credential check.
I tried monkeypatch & fastapi_dep fixtures, also patching but somehow the get_item method mocking isn't working
Will mocking get_item method work?
class MockDynamoDBRepository():
def get_item(*args, **kwargs):
return {
"Item": {
"id": "foo",
"title": "Foo",
"description": "Hi",
}
}
class MockAppConfig:
def __init__(self) -> None:
"""Constructor class to instantiate dynamodb and lambda"""
self._job_table = "Dynamo_DB_Table"
self._region = "Table_region"
self._dynamodb_repository = MockDynamoDBRepository(table=self._job_table, region=self._region)
def test_get_request():
fast_api_app.dependency_overrides[AppConfig] = MockAppConfig
response = client.get("/objects/1234")
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "Hi",
}
Building on #svfat's answer, here is how you can do the test with fastapi_dep - pick any one of the test approaches - with clause or indirect parameter:
class MockDynamoDBRepository():
def __init__(self, *args, **kwargs):
pass
def get_item(self, *args, **kwargs):
return {
"Item": {
"id": "foo",
"title": "Foo",
"description": "Hi",
}
}
class MockAppConfig:
def __init__(self) -> None:
"""Constructor class to instantiate dynamodb and lambda"""
self._job_table = "Mock Dynamo_DB_Table"
self._region = "Mock Table_region"
self._dynamodb_repository = MockDynamoDBRepository(table=self._job_table,
region=self._region)
def test_get_request_deps(fastapi_dep):
with fastapi_dep(fast_api_app).override(
{
AppConfig: MockAppConfig,
}
):
response = client.get("/objects/1234")
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "Hi",
}
#pytest.mark.parametrize(
"fastapi_dep",
[
(
fast_api_app,
{AppConfig: MockAppConfig},
)
],
indirect=True,
)
def test_get_request_deps_indirect(fastapi_dep):
response = client.get("/objects/1234")
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "Hi",
}
If you don't want to create all the extra classes, you can use the pure mock approach like so:
from mock.mock import MagicMock
def test_get_request_deps_mock(fastapi_dep):
my_mock = MagicMock()
my_mock._dynamodb_repository.get_item.return_value = {
"Item": {
"id": "foo",
"title": "Foo",
"description": "Hi",
}
}
with fastapi_dep(file_3.app).override(
{
AppConfig: lambda: my_mock,
}
):
response = client.get("/objects/1234")
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "Hi",
}
Related
I have this piece of code
import json
class Object:
def toJSON(self):
return json.dumps(self, default=lambda o: o.__dict__,
sort_keys=False, indent=4)
languages={"pl_PL":"1","en_US":"2","de_DE":"4","fr_FR":"10","cs_CZ":"6"}
def generateJSON():
product=Object()
for lang in languages.keys():
product.translations = Object()
product.translations.lang=Object()
product.translations.lang.name = "xxx"
print(product.toJSON())
if __name__ == '__main__':
generateJSON()
Which gives me an output:
{
"translations": {
"lang": {
"name": "xxx"
}
} }
How can I assing lang value to my Object name to get the output:
{
"translations": {
"pl_PL": {
"name": "xxx"
},
{
"en_US": {
"name": "xxx"
}
}etc... }
To use the string names in lang each time round the loop you need to use setattr() and getattr():
import json
class Object:
def toJSON(self):
return json.dumps(self, default=lambda o: o.__dict__,
sort_keys=False, indent=4)
languages={"pl_PL":"1","en_US":"2","de_DE":"4","fr_FR":"10","cs_CZ":"6"}
def generateJSON():
product=Object()
product.translations = Object()
for lang in languages.keys():
setattr(product.translations,lang,Object())
getattr(product.translations,lang).name = "xxx"
print(product.toJSON())
if __name__ == '__main__':
generateJSON()
Output as requested.
I am trying to mock some S3 operations and after banging my head against the stubber object, I tried doing something as follows:
def mock_make_api_call(self, operation_name, kwarg):
if operation_name == "ListObjectsV2":
return {
"KeyCount": 1,
"Contents": [
{"Key": "sensor_1", "LastModified": "2021-11-30T12:58:14+00:00"}
],
}
elif operation_name == "GetObjectTagging":
return {"TagSet": []}
elif operation_name == "HeadObject":
return {
"ContentLength": 10,
"ContentType": "gzip",
"ResponseMetadata": {
"Bucket": "1",
},
}
elif operation_name == "GetObject":
content = get_object_response()
return {
"ContentLength": len(content),
"ContentType": "xml",
"ContentEncoding": "gzip",
"Body": content,
"ResponseMetadata": {
"Bucket": "1",
},
}
Ot is the s3 download_fileoperation which is giving me a headache. As far as I can tell it generates, the HeadObjectand GetObjectcalls.
My content generation method is as follows:
def get_object_response():
content = b"<some-valid-xml>"
buf = BytesIO()
compressed = gzip.GzipFile(fileobj=buf, mode="wb")
compressed.write(content)
compressed.close()
return buf.getvalue()
The way it gets used is:
with NamedTemporaryFile() as tmp:
s3_client.download_file(Bucket=..., Key=..., Filename=tmp.name)
However, my test fails with:
elf = <s3transfer.utils.StreamReaderProgress object at 0x116a77820>
args = (262144,), kwargs = {}
def read(self, *args, **kwargs):
> value = self._stream.read(*args, **kwargs)
E AttributeError: 'bytes' object has no attribute 'read'
I simply cannot figure out how to encode the response so that the generated content can be saved.
How can I use an L2 Secret created with Secrets Manager to resolve as an L1 Cfn Property value?
from aws_cdk import (
core,
aws_secretsmanager as secretsmanager,
aws_elasticache as elasticache
)
class MyStack(core.Stack):
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
redis_password = secretsmanager.Secret(
self, "RedisPassword",
description="Redis auth",
generate_secret_string=secretsmanager.SecretStringGenerator(
exclude_characters='/"#'
)
)
self.redis = elasticache.CfnReplicationGroup(self, 'RedisCluster',
auth_token=redis_password.secret_value,
# other properties
)
This gives the error
jsii.errors.JSIIError: Object of type #aws-cdk/aws-secretsmanager.Secret is not convertible to #aws-cdk/core.CfnElement
In Cloudformation to resolve a secret I'd use something like
AuthToken: !Sub '{{resolve:secretsmanager:${MySecret}::password}}'
But a L2 Secret doesn't output the Cfn Ref like L1 constructs do (that I know of)
What am I missing?
I was only missing the to_string() method
from aws_cdk import (
core,
aws_secretsmanager as secretsmanager,
aws_elasticache as elasticache
)
class MyStack(core.Stack):
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
redis_password = secretsmanager.Secret(
self, "RedisPassword",
description="Redis auth",
generate_secret_string=secretsmanager.SecretStringGenerator(
exclude_characters='/"#'
)
)
self.redis = elasticache.CfnReplicationGroup(self, 'RedisCluster',
auth_token=redis_password.secret_value.to_string(),
# other properties
)
This synthesizes to
{
"RedisPasswordED621C10": {
"Type": "AWS::SecretsManager::Secret",
"Properties": {
"Description": "Redis auth",
"GenerateSecretString": {
"ExcludeCharacters": "/\"#"
}
},
"Metadata": {
"aws:cdk:path": "my-cdk-stack/RedisPassword/Resource"
}
},
"RedisCluster": {
"Type": "AWS::ElastiCache::ReplicationGroup",
"Properties": {
"ReplicationGroupDescription": "RedisGroup",
"AtRestEncryptionEnabled": true,
"AuthToken": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "RedisPasswordED621C10"
},
":SecretString:::}}"
]
]
},
"OtherProps": "..."
}
}
}
I want to use the name and description present in tags_metadata inside the method. I don't how to do that is there any way I can use those attributes
from fastapi import FastAPI
tags_metadata = [
{
"name": "select name",
"description": "Operations with users. The **login** logic is also here.",
},
{
"name": "items",
"description": "Manage items. So _fancy_ they have their own docs.",
"externalDocs": {
"description": "Items external docs",
"url": "https://fastapi.tiangolo.com/",
},
},
]
app = FastAPI(openapi_tags=tags_metadata)
#app.get("/users/", tags=["users"])
async def get_users():
return [{"name": "Harry"}, {"name": "Ron"}]
#app.get("/items/", tags=["items"])
async def get_items():
return [{"name": "wand"}, {"name": "flying broom"}]
here I want to select the first name attribute from tags_metadata in the get_users() method
output need to be -- > "select name"
https://fastapi.tiangolo.com/tutorial/metadata/?h=+tags#use-your-tags
Yes there is.
from fastapi import FastAPI
app = FastAPI()
#app.get("/dummy", tags=["dummy2"])
async def dummy():
...
#app.post("/dummy2", tags=["dummy2"])
async def dummy2():
...
Let's assume we have these routes. By checking each route from app.router, we can find the paths.
#app.get("/tags", tags=["tags"])
async def tags():
tags = {}
for route in app.router.__dict__["routes"]:
if hasattr(route, "tags"):
tags[route.__dict__["path"]] = route.__dict__["tags"]
return tags
When we hit the /tags endpoint we will see this.
{
"/dummy":[
"dummy2"
],
"/dummy2":[
"dummy2"
],
"/tags":[
"tags"
]
}
I have a class A where it stores a collection of variables of type B, how can I serialize class A to JSON properly?
example:
class A:
def __init__(self):
self.b_collection = []
#...
class B:
def __init__(self):
# ...
pass
and add instances of B into the collection:
a = A()
a.b_collection = [B(), B(), B()]
when I try to serialize a with json.dumps(a) I get this error: Object of type A is not JSON serializable.
Is there a way to specify how to encoder should encode that class?
something like
def __encode__(self, encoder):
encoder.start_obj()
encoder.add_property('name', self.value)
encoder.add_property('age', self.age)
encoder.end_obj()
which would return something like
{
name: 'Tomer',
age: '19'
}
You can extend json.JSONEncoder to define how to serialize your objects. The default method of your subclass will take a Python object as an argument. You can return a new object that is (hopefully) encodable, or pass the object on to the parent in hopes that it knows how to encode the object.
For example,
class A:
def __init__(self):
self.b_collection = []
class B:
def __init__(self, name, age):
self.name = name
self.age = age
class ABEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, A):
return {'__A__': obj.b_collection}
elif isinstance(obj, B):
return {'__B__': obj.__dict__}
return super().default(obj)
a = A()
a.b_collection.append(B("Tomer", "19"))
a.b_collection.append(B("Bob", "21"))
a.b_collection.append(B("Alice", "23"))
print(json.dumps(a, cls=ABEncoder, indent=4))
would produce
{
"__A__": [
{
"__B__": {
"name": "Tomer",
"age": "19"
}
},
{
"__B__": {
"name": "Bob",
"age": "21"
}
},
{
"__B__": {
"name": "Alice",
"age": "23"
}
}
]
}
Note that you can handle A and B separately; you don't have to first encode the B objects before returning the encodable form of A; the B objects will be encoded later when the list itself is encoded.
The extra objects make it easier to write a decoder; you don't have to make it this complicated if you don't want to be able to decode the JSON to an instance of A. Instead, you can just define
class ABEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, A):
return obj.b_collection
elif isinstance(obj, B):
return obj.__dict__
return super().default(obj)
to get
[
{
"name": "Tomer",
"age": "19"
},
{
"name": "Bob",
"age": "21"
},
{
"name": "Alice",
"age": "23"
}
]
Maybe data classes can help:
from dataclasses import asdict, dataclass
from typing import List
import json
#dataclass
class Person:
name: str
age: str
#dataclass
class Group:
persons: List[Person]
data = [
{'name': 'Tomer', 'age': '19'},
{'name': 'Ivan', 'age': '20'}
]
persons = [Person(**person) for person in data]
group = Group(persons=persons)
assert json.dumps(asdict(group)) == '{"persons": [{"name": "Tomer", "age": "19"}, {"name": "Ivan", "age": "20"}]}'
persons = [asdict(person) for person in group.persons]
assert persons == [{"name": "Tomer", "age": "19"}, {"name": "Ivan", "age": "20"}]
assert json.dumps(persons) == '[{"name": "Tomer", "age": "19"}, {"name": "Ivan", "age": "20"}]'