Skip to content

Commit

Permalink
Add unit tests for ImageService #29
Browse files Browse the repository at this point in the history
  • Loading branch information
joelvdavies committed Oct 7, 2024
1 parent 4a00a96 commit 9b971fc
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 4 deletions.
38 changes: 36 additions & 2 deletions test/unit/services/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import pytest

from object_storage_api.repositories.attachment import AttachmentRepo
from object_storage_api.repositories.image import ImageRepo
from object_storage_api.services.attachment import AttachmentService
from object_storage_api.services.image import ImageService
from object_storage_api.stores.attachment import AttachmentStore
from object_storage_api.stores.image import ImageStore


@pytest.fixture(name="attachment_repository_mock")
Expand All @@ -22,6 +25,16 @@ def fixture_attachment_repository_mock() -> Mock:
return Mock(AttachmentRepo)


@pytest.fixture(name="image_repository_mock")
def fixture_image_repository_mock() -> Mock:
"""
Fixture to create a mock of the `ImageRepo` dependency.
:return: Mocked `ImageRepo` instance.
"""
return Mock(ImageRepo)


@pytest.fixture(name="attachment_store_mock")
def fixture_attachment_store_mock() -> Mock:
"""
Expand All @@ -32,11 +45,20 @@ def fixture_attachment_store_mock() -> Mock:
return Mock(AttachmentStore)


@pytest.fixture(name="image_store_mock")
def fixture_image_store_mock() -> Mock:
"""
Fixture to create a mock of the `ImageStore` dependency.
:return: Mocked `ImageStore` instance.
"""
return Mock(ImageStore)


@pytest.fixture(name="attachment_service")
def fixture_attachment_service(attachment_repository_mock: Mock, attachment_store_mock: Mock) -> AttachmentService:
"""
Fixture to create a `AttachmentService` instance with mocked `AttachmentRepo` and `AttachmentStore`
dependencies.
Fixture to create a `AttachmentService` instance with mocked `AttachmentRepo` and `AttachmentStore` dependencies.
:param attachment_repository_mock: Mocked `AttachmentRepo` instance.
:param attachment_store_mock: Mocked `AttachmentStore` instance.
Expand All @@ -45,6 +67,18 @@ def fixture_attachment_service(attachment_repository_mock: Mock, attachment_stor
return AttachmentService(attachment_repository_mock, attachment_store_mock)


@pytest.fixture(name="image_service")
def fixture_image_service(image_repository_mock: Mock, image_store_mock: Mock) -> AttachmentService:
"""
Fixture to create a `ImageService` instance with mocked `ImageRepo` and `ImageStore` dependencies.
:param image_repository_mock: Mocked `ImageRepo` instance.
:param image_store_mock: Mocked `ImageStore` instance.
:return: `ImageService` instance with the mocked dependencies.
"""
return ImageService(image_repository_mock, image_store_mock)


MODEL_MIXINS_FIXED_DATETIME_NOW = datetime(2024, 2, 16, 14, 0, 0, 0, tzinfo=timezone.utc)


Expand Down
2 changes: 1 addition & 1 deletion test/unit/services/test_attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class CreateDSL(AttachmentServiceDSL):

_attachment_post: AttachmentPostSchema
_expected_attachment_id: ObjectId
_expected_attachment_in: MagicMock
_expected_attachment_in: AttachmentIn
_expected_attachment: AttachmentPostResponseSchema
_created_attachment: AttachmentPostResponseSchema
_create_exception: pytest.ExceptionInfo
Expand Down
150 changes: 150 additions & 0 deletions test/unit/services/test_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""
Unit tests for the `ImageService` service.
"""

from test.mock_data import IMAGE_POST_METADATA_DATA_ALL_VALUES
from unittest.mock import MagicMock, Mock, patch

import pytest
from bson import ObjectId
from fastapi import UploadFile

from object_storage_api.core.exceptions import InvalidObjectIdError
from object_storage_api.models.image import ImageIn, ImageOut
from object_storage_api.schemas.image import ImagePostMetadataSchema, ImageSchema
from object_storage_api.services.image import ImageService


class ImageServiceDSL:
"""Base class for `ImageService` unit tests."""

mock_image_repository: Mock
mock_image_store: Mock
image_service: ImageService

mock_object_id: MagicMock

@pytest.fixture(autouse=True)
def setup(
self,
image_repository_mock,
image_store_mock,
image_service,
# Ensures all created and modified times are mocked throughout
# pylint: disable=unused-argument
model_mixins_datetime_now_mock,
):
"""Setup fixtures"""

self.mock_image_repository = image_repository_mock
self.mock_image_store = image_store_mock
self.image_service = image_service

with patch("object_storage_api.services.image.ObjectId") as object_id_mock:
self.mock_object_id = object_id_mock
yield


class CreateDSL(ImageServiceDSL):
"""Base class for `create` tests."""

_image_post_metadata: ImagePostMetadataSchema
_upload_file: UploadFile
_expected_image_id: ObjectId
_expected_image_in: ImageIn
_expected_image: ImageSchema
_created_image: ImageSchema
_create_exception: pytest.ExceptionInfo

def mock_create(self, image_post_metadata_data: dict) -> None:
"""
Mocks repo & store methods appropriately to test the `create` service method.
:param image_post_metadata_data: Dictionary containing the image data as would be required for an
`ImagePostMetadataSchema`.
"""

self._image_post_metadata = ImagePostMetadataSchema(**image_post_metadata_data)
self._upload_file = UploadFile(MagicMock(), size=100, filename="test.png", headers=MagicMock())

self._expected_image_id = ObjectId()
self.mock_object_id.return_value = self._expected_image_id

# Store
expected_object_key = "some/object/key"
self.mock_image_store.upload.return_value = expected_object_key

# Expected model data with the object key defined (Ignore if invalid to avoid a premature error)
if self._image_post_metadata.entity_id != "invalid-id":
self._expected_image_in = ImageIn(
**self._image_post_metadata.model_dump(),
id=str(self._expected_image_id),
object_key=expected_object_key,
file_name=self._upload_file.filename,
)

# Repo (The contents of the returned output model does not matter here as long as its valid)
expected_image_out = ImageOut(**self._expected_image_in.model_dump(by_alias=True))
self.mock_image_repository.create.return_value = expected_image_out

self._expected_image = ImageSchema(**expected_image_out.model_dump())

def call_create(self) -> None:
"""Calls the `ImageService` `create` method with the appropriate data from a prior call to
`mock_create`."""

self._created_image = self.image_service.create(self._image_post_metadata, self._upload_file)

def call_create_expecting_error(self, error_type: type[BaseException]) -> None:
"""Calls the `ImageService` `create` method with the appropriate data from a prior call to
`mock_create` while expecting an error to be raised..
:param error_type: Expected exception to be raised.
"""

with pytest.raises(error_type) as exc:
self.image_service.create(self._image_post_metadata, self._upload_file)
self._create_exception = exc

def check_create_success(self) -> None:
"""Checks that a prior call to `call_create` worked as expected."""

self.mock_image_store.upload.assert_called_once_with(
str(self._expected_image_id), self._image_post_metadata, self._upload_file
)
self.mock_image_repository.create.assert_called_once_with(self._expected_image_in)

assert self._created_image == self._expected_image

def check_create_failed_with_exception(self, message: str) -> None:
"""
Checks that a prior call to `call_create_expecting_error` worked as expected, raising an exception
with the correct message.
:param message: Message of the raised exception.
"""

self.mock_image_store.upload.assert_called_once_with(
str(self._expected_image_id), self._image_post_metadata, self._upload_file
)
self.mock_image_repository.create.assert_not_called()

assert str(self._create_exception.value) == message


class TestCreate(CreateDSL):
"""Tests for creating an image."""

def test_create(self):
"""Test creating an image."""

self.mock_create(IMAGE_POST_METADATA_DATA_ALL_VALUES)
self.call_create()
self.check_create_success()

def test_create_with_invalid_entity_id(self):
"""Test creating an image with an invalid `entity_id`."""

self.mock_create({**IMAGE_POST_METADATA_DATA_ALL_VALUES, "entity_id": "invalid-id"})
self.call_create_expecting_error(InvalidObjectIdError)
self.check_create_failed_with_exception("Invalid ObjectId value 'invalid-id'")
2 changes: 1 addition & 1 deletion test/unit/stores/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def mock_upload(self, image_post_metadata_data: dict) -> None:
"""
Mocks object store methods appropriately to test the `upload` store method.
:param image_post_metadata_data: Dictionary containing the attachment data as would be required for an
:param image_post_metadata_data: Dictionary containing the image data as would be required for an
`ImagePostMetadataSchema`.
"""

Expand Down

0 comments on commit 9b971fc

Please sign in to comment.