From 911c877d8efd681427e59275bd9e33b830070fff Mon Sep 17 00:00:00 2001 From: jishnuc Date: Wed, 23 Sep 2026 18:34:19 +0530 Subject: [PATCH 1/4] test: add player service unit tests --- tests/test_player_service.py | 200 +++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 tests/test_player_service.py diff --git a/tests/test_player_service.py b/tests/test_player_service.py new file mode 100644 index 0000000..62a2b3e --- /dev/null +++ b/tests/test_player_service.py @@ -0,0 +1,200 @@ +""" +Test cases for the PlayerService. +""" + +from unittest.mock import AsyncMock, Mock +from uuid import UUID + +import pytest +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.ext.asyncio import AsyncSession + +from models.player_model import PlayerRequestModel +from schemas.player_schema import Player +from services.player_service import ( + create_async, + retrieve_all_async, + retrieve_by_id_async, + retrieve_by_squad_number_async, +) +from tests.player_fake import existing_player, nonexistent_player, unknown_player + + +@pytest.fixture +def existing_player_request_model(): + """ + Fixture for an existing PlayerRequestModel. + """ + existing_player_data = existing_player() + return PlayerRequestModel( + first_name=existing_player_data.first_name + if existing_player_data.first_name + else "", + middle_name=existing_player_data.middle_name, + last_name=existing_player_data.last_name + if existing_player_data.last_name + else "", + date_of_birth=existing_player_data.date_of_birth, + squad_number=existing_player_data.squad_number + if existing_player_data.squad_number + else 0, + position=existing_player_data.position if existing_player_data.position else "", + abbr_position=existing_player_data.abbr_position, + team=existing_player_data.team, + league=existing_player_data.league, + starting11=existing_player_data.starting11, + ) + + +@pytest.fixture +def existing_player_schema(): + """ + Fixture for an existing Player schema. + """ + existing_player_data = existing_player() + return Player( + id=UUID(existing_player_data.id), + first_name=existing_player_data.first_name, + middle_name=existing_player_data.middle_name, + last_name=existing_player_data.last_name, + date_of_birth=existing_player_data.date_of_birth, + squad_number=existing_player_data.squad_number, + position=existing_player_data.position, + abbr_position=existing_player_data.abbr_position, + team=existing_player_data.team, + league=existing_player_data.league, + starting11=existing_player_data.starting11, + ) + + +@pytest.mark.anyio +async def test_create_async_success(existing_player_request_model): + """ + Test the successful creation of a Player. + """ + mock_session = AsyncMock(spec=AsyncSession) + + result = await create_async(mock_session, existing_player_request_model) + + assert result is not None + mock_session.add.assert_called_once_with(result) + mock_session.commit.assert_awaited_once() + mock_session.refresh.assert_awaited_once_with(result) + + +@pytest.mark.anyio +async def test_create_async_database_error(existing_player_request_model): + mock_session = AsyncMock(spec=AsyncSession) + mock_session.commit.side_effect = SQLAlchemyError("Connection timeout") + + result = await create_async(mock_session, existing_player_request_model) + + assert result is None + mock_session.commit.assert_awaited_once() + mock_session.rollback.assert_awaited_once() + mock_session.refresh.assert_not_awaited() + + +@pytest.mark.anyio +async def test_retrieve_all_async(): + """ + Test the retrieval of all players. + """ + mock_session = AsyncMock(spec=AsyncSession) + mock_result = Mock() + mock_players = [existing_player(), nonexistent_player(), unknown_player()] + mock_result.scalars.return_value.all.return_value = mock_players + + mock_session.execute.return_value = mock_result + + result = await retrieve_all_async(mock_session) + + assert result == mock_players + mock_session.execute.assert_awaited_once() + + +@pytest.mark.anyio +async def test_retrieve_all_async_empty(): + """ + Test the retrieval of all players when no players exist. + """ + mock_session = AsyncMock(spec=AsyncSession) + mock_result = Mock() + mock_result.scalars.return_value.all.return_value = [] + + mock_session.execute.return_value = mock_result + + result = await retrieve_all_async(mock_session) + + assert result == [] + mock_session.execute.assert_awaited_once() + + +@pytest.mark.anyio +async def test_retrieve_by_id_async(existing_player_schema): + """ + Test the retrieval of a player by ID. + """ + mock_session = AsyncMock(spec=AsyncSession) + + mock_session.get.return_value = existing_player_schema + + result = await retrieve_by_id_async(mock_session, existing_player_schema.id) # type: ignore + + assert result == existing_player_schema + mock_session.get.assert_awaited_once_with(Player, existing_player_schema.id) + + +@pytest.mark.anyio +async def test_retrieve_by_id_async_not_found(): + """ + Test the retrieval of a player by ID when the player does not exist. + """ + mock_session = AsyncMock(spec=AsyncSession) + non_existent_id = UUID("00000000-0000-0000-0000-000000000000") + + mock_session.get.return_value = None + + result = await retrieve_by_id_async(mock_session, non_existent_id) + + assert result is None + mock_session.get.assert_awaited_once_with(Player, non_existent_id) + + +@pytest.mark.anyio +async def test_retrieve_by_squad_number_async(existing_player_schema): + """ + Test the retrieval of a player by Squad Number. + """ + mock_session = AsyncMock(spec=AsyncSession) + mock_result = Mock() + mock_result.scalars.return_value.first.return_value = existing_player_schema + + mock_session.execute.return_value = mock_result + + result = await retrieve_by_squad_number_async( + mock_session, + existing_player_schema.squad_number, # type: ignore + ) + + assert result == existing_player_schema + mock_session.execute.assert_awaited_once() + +@pytest.mark.anyio +async def test_retrieve_by_squad_number_async_not_found(): + """ + Test the retrieval of a player by Squad Number when the player does not exist. + """ + mock_session = AsyncMock(spec=AsyncSession) + non_existent_squad_number = 99 + + mock_result = Mock() + mock_result.scalars.return_value.first.return_value = None + mock_session.execute.return_value = mock_result + + result = await retrieve_by_squad_number_async( + mock_session, non_existent_squad_number + ) + + assert result is None + mock_session.execute.assert_awaited_once() From cf889031f06c28f33aa86239f3d9a267bbf1d302 Mon Sep 17 00:00:00 2001 From: jishnuc Date: Thu, 24 Sep 2026 13:18:26 +0530 Subject: [PATCH 2/4] test: add player service unit tests --- tests/test_player_service.py | 227 +++++++++++++++++++++++++++++++++-- 1 file changed, 215 insertions(+), 12 deletions(-) diff --git a/tests/test_player_service.py b/tests/test_player_service.py index 62a2b3e..a708aa4 100644 --- a/tests/test_player_service.py +++ b/tests/test_player_service.py @@ -2,7 +2,7 @@ Test cases for the PlayerService. """ -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch from uuid import UUID import pytest @@ -13,9 +13,11 @@ from schemas.player_schema import Player from services.player_service import ( create_async, + delete_by_squad_number_async, retrieve_all_async, retrieve_by_id_async, retrieve_by_squad_number_async, + update_by_squad_number_async, ) from tests.player_fake import existing_player, nonexistent_player, unknown_player @@ -27,17 +29,19 @@ def existing_player_request_model(): """ existing_player_data = existing_player() return PlayerRequestModel( - first_name=existing_player_data.first_name - if existing_player_data.first_name - else "", + first_name=( + existing_player_data.first_name if existing_player_data.first_name else "" + ), middle_name=existing_player_data.middle_name, - last_name=existing_player_data.last_name - if existing_player_data.last_name - else "", + last_name=( + existing_player_data.last_name if existing_player_data.last_name else "" + ), date_of_birth=existing_player_data.date_of_birth, - squad_number=existing_player_data.squad_number - if existing_player_data.squad_number - else 0, + squad_number=( + existing_player_data.squad_number + if existing_player_data.squad_number + else 0 + ), position=existing_player_data.position if existing_player_data.position else "", abbr_position=existing_player_data.abbr_position, team=existing_player_data.team, @@ -139,7 +143,7 @@ async def test_retrieve_by_id_async(existing_player_schema): mock_session.get.return_value = existing_player_schema - result = await retrieve_by_id_async(mock_session, existing_player_schema.id) # type: ignore + result = await retrieve_by_id_async(mock_session, existing_player_schema.id) assert result == existing_player_schema mock_session.get.assert_awaited_once_with(Player, existing_player_schema.id) @@ -174,12 +178,13 @@ async def test_retrieve_by_squad_number_async(existing_player_schema): result = await retrieve_by_squad_number_async( mock_session, - existing_player_schema.squad_number, # type: ignore + existing_player_schema.squad_number, ) assert result == existing_player_schema mock_session.execute.assert_awaited_once() + @pytest.mark.anyio async def test_retrieve_by_squad_number_async_not_found(): """ @@ -198,3 +203,201 @@ async def test_retrieve_by_squad_number_async_not_found(): assert result is None mock_session.execute.assert_awaited_once() + + +@pytest.mark.anyio +async def test_update_by_squad_number_async_success( + existing_player_request_model, existing_player_schema +): + """ + Test the successful update of a player by Squad Number. + """ + mock_session = AsyncMock(spec=AsyncSession) + + existing_player_request_model.first_name = "UpdatedFirstName" + existing_player_request_model.middle_name = "UpdatedMiddleName" + existing_player_request_model.last_name = "UpdatedLastName" + existing_player_request_model.position = "UpdatedPosition" + existing_player_request_model.abbr_position = "UPD" + existing_player_request_model.team = "UpdatedTeam" + existing_player_request_model.league = "UpdatedLeague" + existing_player_request_model.starting11 = not existing_player_schema.starting11 + + with patch( + "services.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve: + mock_retrieve.return_value = existing_player_schema + + result = await update_by_squad_number_async( + mock_session, + existing_player_schema.squad_number, + existing_player_request_model, + ) + + assert result is True + mock_retrieve.assert_awaited_once_with( + mock_session, existing_player_schema.squad_number + ) + mock_session.commit.assert_awaited_once() + mock_session.rollback.assert_not_awaited() + + assert existing_player_schema.first_name == existing_player_request_model.first_name + assert ( + existing_player_schema.middle_name == existing_player_request_model.middle_name + ) + assert existing_player_schema.last_name == existing_player_request_model.last_name + assert ( + existing_player_schema.date_of_birth + == existing_player_request_model.date_of_birth + ) + assert ( + existing_player_schema.squad_number + == existing_player_request_model.squad_number + ) + assert existing_player_schema.position == existing_player_request_model.position + assert ( + existing_player_schema.abbr_position + == existing_player_request_model.abbr_position + ) + assert existing_player_schema.team == existing_player_request_model.team + assert existing_player_schema.league == existing_player_request_model.league + assert existing_player_schema.starting11 == existing_player_request_model.starting11 + + +@pytest.mark.anyio +async def test_update_by_squad_number_async_not_found( + existing_player_request_model, +): + """ + Test the update of a player by Squad Number when the player does not exist. + """ + mock_session = AsyncMock(spec=AsyncSession) + non_existent_squad_number = 99 + + with patch( + "services.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve: + mock_retrieve.return_value = None + + result = await update_by_squad_number_async( + mock_session, + non_existent_squad_number, + existing_player_request_model, + ) + + assert result is False + mock_retrieve.assert_awaited_once_with(mock_session, non_existent_squad_number) + mock_session.commit.assert_not_awaited() + mock_session.rollback.assert_not_awaited() + + +@pytest.mark.anyio +async def test_update_by_squad_number_async_database_error( + existing_player_request_model, existing_player_schema +): + """ + Test the update of a player by Squad Number when a database error occurs. + """ + mock_session = AsyncMock(spec=AsyncSession) + + with patch( + "services.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve: + mock_retrieve.return_value = existing_player_schema + mock_session.commit.side_effect = SQLAlchemyError("Connection timeout") + + result = await update_by_squad_number_async( + mock_session, + existing_player_schema.squad_number, + existing_player_request_model, + ) + + assert result is False + mock_retrieve.assert_awaited_once_with( + mock_session, existing_player_schema.squad_number + ) + mock_session.commit.assert_awaited_once() + mock_session.rollback.assert_awaited_once() + + +@pytest.mark.anyio +async def test_delete_by_squad_number_async_success(existing_player_schema): + """ + Test the successful deletion of a player by Squad Number. + """ + mock_session = AsyncMock(spec=AsyncSession) + + with patch( + "services.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve: + mock_retrieve.return_value = existing_player_schema + + result = await delete_by_squad_number_async( + mock_session, + existing_player_schema.squad_number, + ) + + assert result is True + mock_retrieve.assert_awaited_once_with( + mock_session, existing_player_schema.squad_number + ) + mock_session.delete.assert_awaited_once_with(existing_player_schema) + mock_session.commit.assert_awaited_once() + mock_session.rollback.assert_not_awaited() + + +@pytest.mark.anyio +async def test_delete_by_squad_number_async_not_found(): + """ + Test the deletion of a player by Squad Number when the player does not exist. + """ + mock_session = AsyncMock(spec=AsyncSession) + non_existent_squad_number = 99 + + with patch( + "services.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve: + mock_retrieve.return_value = None + + result = await delete_by_squad_number_async( + mock_session, non_existent_squad_number + ) + + assert result is False + mock_retrieve.assert_awaited_once_with(mock_session, non_existent_squad_number) + mock_session.delete.assert_not_awaited() + mock_session.commit.assert_not_awaited() + mock_session.rollback.assert_not_awaited() + + +@pytest.mark.anyio +async def test_delete_by_squad_number_async_database_error(existing_player_schema): + """ + Test the deletion of a player by Squad Number when a database error occurs. + """ + mock_session = AsyncMock(spec=AsyncSession) + + with patch( + "services.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve: + mock_retrieve.return_value = existing_player_schema + mock_session.commit.side_effect = SQLAlchemyError("Connection timeout") + + result = await delete_by_squad_number_async( + mock_session, + existing_player_schema.squad_number, + ) + + assert result is False + mock_retrieve.assert_awaited_once_with( + mock_session, existing_player_schema.squad_number + ) + mock_session.delete.assert_awaited_once_with(existing_player_schema) + mock_session.commit.assert_awaited_once() + mock_session.rollback.assert_awaited_once() From 63681380d6ed0d522c7712587ec2b633e1e311de Mon Sep 17 00:00:00 2001 From: jishnuc Date: Sat, 26 Sep 2026 20:24:57 +0530 Subject: [PATCH 3/4] test: add player route unit tests --- tests/test_player_route.py | 604 +++++++++++++++++++++++++++++++++++ tests/test_player_service.py | 52 ++- 2 files changed, 625 insertions(+), 31 deletions(-) create mode 100644 tests/test_player_route.py diff --git a/tests/test_player_route.py b/tests/test_player_route.py new file mode 100644 index 0000000..9c34e54 --- /dev/null +++ b/tests/test_player_route.py @@ -0,0 +1,604 @@ +""" +Unit tests for the Player API routes. +""" + +from unittest.mock import AsyncMock, patch +from uuid import UUID + +import pytest +from fastapi import HTTPException, Response, status +from sqlalchemy.ext.asyncio import AsyncSession + +from models.player_model import PlayerRequestModel +from routes.player_route import ( + CACHE_KEY, + CACHE_TTL, + delete_async, + get_all_async, + get_by_id_async, + get_by_squad_number_async, + post_async, + put_async, + simple_memory_cache, +) +from schemas.player_schema import Player + + +@pytest.fixture +def existing_player_schema(): + return Player( + id=UUID("01772c59-43f0-5d85-b913-c78e4e281452"), + first_name="Damián", + middle_name="Emiliano", + last_name="Martínez", + date_of_birth="1992-09-02T00:00:00.000Z", + squad_number=23, + position="Goalkeeper", + abbr_position="GK", + team="Aston Villa FC", + league="Premier League", + starting11=True, + ) + + +@pytest.fixture +def player_request_model(): + return PlayerRequestModel( + first_name="Giovani", + last_name="Lo Celso", + date_of_birth="1996-07-09T00:00:00.000Z", + squad_number=27, + position="Central Midfield", + abbr_position="CM", + team="Real Betis Balompié", + league="La Liga", + starting11=False, + ) + + +@pytest.fixture +def player_schema(): + return Player( + first_name="Giovani", + last_name="Lo Celso", + date_of_birth="1996-07-09T00:00:00.000Z", + squad_number=27, + position="Central Midfield", + abbr_position="CM", + team="Real Betis Balompié", + league="La Liga", + starting11=False, + ) + + +@pytest.mark.anyio +async def test_request_post_player_body_nonexistent_response_created( + player_request_model, player_schema +): + mock_async_session = AsyncMock(spec=AsyncSession) + response = Response() + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve, + patch( + "routes.player_route.player_service.create_async", + new_callable=AsyncMock, + ) as mock_create, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve.return_value = None + mock_create.return_value = player_schema + + result = await post_async( + player_model=player_request_model, + async_session=mock_async_session, + response=response, + ) + + assert result == player_schema + mock_retrieve.assert_awaited_once_with( + mock_async_session, player_request_model.squad_number + ) + mock_create.assert_awaited_once_with(mock_async_session, player_request_model) + assert ( + response.headers["Location"] + == f"/players/squadnumber/{player_request_model.squad_number}" + ) + mock_clear_cache.assert_awaited_once_with(CACHE_KEY) + + +@pytest.mark.anyio +async def test_request_post_player_body_existing_response_conflict( + player_request_model, player_schema +): + mock_async_session = AsyncMock(spec=AsyncSession) + response = Response() + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve, + patch( + "routes.player_route.player_service.create_async", + new_callable=AsyncMock, + ) as mock_create, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve.return_value = player_schema + + with pytest.raises(HTTPException) as exc_info: + await post_async( + player_model=player_request_model, + async_session=mock_async_session, + response=response, + ) + + assert exc_info.value.status_code == status.HTTP_409_CONFLICT + assert exc_info.value.detail == "A Player with this squad number already exists." + mock_retrieve.assert_awaited_once_with( + mock_async_session, player_request_model.squad_number + ) + mock_create.assert_not_awaited() + mock_clear_cache.assert_not_awaited() + + +@pytest.mark.anyio +async def test_request_post_player_body_service_failure_response_server_error( + player_request_model, +): + mock_async_session = AsyncMock(spec=AsyncSession) + response = Response() + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve, + patch( + "routes.player_route.player_service.create_async", + new_callable=AsyncMock, + ) as mock_create, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve.return_value = None + mock_create.return_value = None + + with pytest.raises(HTTPException) as exc_info: + await post_async( + player_model=player_request_model, + async_session=mock_async_session, + response=response, + ) + + assert exc_info.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert ( + exc_info.value.detail == "Failed to create the Player due to a database error." + ) + mock_retrieve.assert_awaited_once_with( + mock_async_session, player_request_model.squad_number + ) + mock_create.assert_awaited_once_with(mock_async_session, player_request_model) + mock_clear_cache.assert_not_awaited() + + +@pytest.mark.anyio +async def test_request_get_players_cache_hit_response_success(player_schema): + mock_async_session = AsyncMock(spec=AsyncSession) + response = Response() + + with ( + patch.object( + simple_memory_cache, "get", new_callable=AsyncMock + ) as mock_get_cache, + patch( + "routes.player_route.player_service.retrieve_all_async", + new_callable=AsyncMock, + ) as mock_retrieve_all, + ): + mock_get_cache.return_value = [player_schema] + + result = await get_all_async( + response=response, async_session=mock_async_session + ) + + assert result == [player_schema] + assert response.headers["X-Cache"] == "HIT" + mock_get_cache.assert_awaited_once_with(CACHE_KEY) + mock_retrieve_all.assert_not_awaited() + + +@pytest.mark.anyio +async def test_request_get_players_cache_miss_response_success(player_schema): + mock_async_session = AsyncMock(spec=AsyncSession) + response = Response() + + with ( + patch.object( + simple_memory_cache, "get", new_callable=AsyncMock + ) as mock_get_cache, + patch( + "routes.player_route.player_service.retrieve_all_async", + new_callable=AsyncMock, + ) as mock_retrieve_all, + patch.object( + simple_memory_cache, "set", new_callable=AsyncMock + ) as mock_set_cache, + ): + mock_get_cache.return_value = None + mock_retrieve_all.return_value = [player_schema] + + result = await get_all_async( + response=response, async_session=mock_async_session + ) + + assert result == [player_schema] + assert response.headers["X-Cache"] == "MISS" + mock_get_cache.assert_awaited_once_with(CACHE_KEY) + mock_retrieve_all.assert_awaited_once_with(mock_async_session) + mock_set_cache.assert_awaited_once_with(CACHE_KEY, [player_schema], ttl=CACHE_TTL) + + +@pytest.mark.anyio +async def test_request_get_player_id_existing_response_success(existing_player_schema): + mock_async_session = AsyncMock(spec=AsyncSession) + + with patch( + "routes.player_route.player_service.retrieve_by_id_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_id: + mock_retrieve_by_id.return_value = existing_player_schema + + result = await get_by_id_async( + player_id=existing_player_schema.id, + async_session=mock_async_session, + ) + + assert result == existing_player_schema + mock_retrieve_by_id.assert_awaited_once_with( + mock_async_session, existing_player_schema.id + ) + + +@pytest.mark.anyio +async def test_request_get_player_id_unknown_response_not_found(): + mock_async_session = AsyncMock(spec=AsyncSession) + unknown_player_id = UUID("00000000-0000-0000-0000-000000000000") + + with patch( + "routes.player_route.player_service.retrieve_by_id_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_id: + mock_retrieve_by_id.return_value = None + + with pytest.raises(HTTPException) as exc_info: + await get_by_id_async( + player_id=unknown_player_id, + async_session=mock_async_session, + ) + + assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND + mock_retrieve_by_id.assert_awaited_once_with(mock_async_session, unknown_player_id) + + +@pytest.mark.anyio +async def test_request_get_player_squadnumber_existing_response_success( + existing_player_schema, +): + mock_async_session = AsyncMock(spec=AsyncSession) + + with patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number: + mock_retrieve_by_squad_number.return_value = existing_player_schema + + result = await get_by_squad_number_async( + squad_number=existing_player_schema.squad_number, + async_session=mock_async_session, + ) + + assert result == existing_player_schema + mock_retrieve_by_squad_number.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number + ) + + +@pytest.mark.anyio +async def test_request_get_player_squadnumber_unknown_response_not_found(): + mock_async_session = AsyncMock(spec=AsyncSession) + unknown_squad_number = 99 + + with patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number: + mock_retrieve_by_squad_number.return_value = None + + with pytest.raises(HTTPException) as exc_info: + await get_by_squad_number_async( + squad_number=unknown_squad_number, + async_session=mock_async_session, + ) + + assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND + mock_retrieve_by_squad_number.assert_awaited_once_with( + mock_async_session, unknown_squad_number + ) + + +@pytest.mark.anyio +async def test_request_put_player_squadnumber_existing_response_no_content( + existing_player_schema, player_request_model +): + + player_request_model.squad_number = existing_player_schema.squad_number + mock_async_session = AsyncMock(spec=AsyncSession) + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number, + patch( + "routes.player_route.player_service.update_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_update, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve_by_squad_number.return_value = existing_player_schema + mock_update.return_value = True + + result = await put_async( + squad_number=existing_player_schema.squad_number, + player_model=player_request_model, + async_session=mock_async_session, + ) + + assert result is None + mock_retrieve_by_squad_number.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number + ) + mock_update.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number, player_request_model + ) + mock_clear_cache.assert_awaited_once_with(CACHE_KEY) + + +@pytest.mark.anyio +async def test_request_put_player_squadnumber_mismatch_response_bad_request( + existing_player_schema, player_request_model +): + + player_request_model.squad_number = existing_player_schema.squad_number + 1 + mock_async_session = AsyncMock(spec=AsyncSession) + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number, + patch( + "routes.player_route.player_service.update_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_update, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve_by_squad_number.return_value = existing_player_schema + + with pytest.raises(HTTPException) as exc_info: + await put_async( + squad_number=existing_player_schema.squad_number, + player_model=player_request_model, + async_session=mock_async_session, + ) + + assert exc_info.value.status_code == status.HTTP_400_BAD_REQUEST + mock_retrieve_by_squad_number.assert_not_awaited() + mock_update.assert_not_awaited() + mock_clear_cache.assert_not_awaited() + + +@pytest.mark.anyio +async def test_request_put_player_squadnumber_unknown_response_not_found( + player_request_model, +): + mock_async_session = AsyncMock(spec=AsyncSession) + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number, + patch( + "routes.player_route.player_service.update_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_update, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve_by_squad_number.return_value = None + + with pytest.raises(HTTPException) as exc_info: + await put_async( + squad_number=player_request_model.squad_number, + player_model=player_request_model, + async_session=mock_async_session, + ) + + assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND + mock_retrieve_by_squad_number.assert_awaited_once_with( + mock_async_session, player_request_model.squad_number + ) + mock_update.assert_not_awaited() + mock_clear_cache.assert_not_awaited() + + +@pytest.mark.anyio +async def test_request_put_player_squadnumber_service_failure_response_server_error( + existing_player_schema, player_request_model +): + + player_request_model.squad_number = existing_player_schema.squad_number + mock_async_session = AsyncMock(spec=AsyncSession) + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number, + patch( + "routes.player_route.player_service.update_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_update, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve_by_squad_number.return_value = existing_player_schema + mock_update.return_value = False + + with pytest.raises(HTTPException) as exc_info: + await put_async( + squad_number=existing_player_schema.squad_number, + player_model=player_request_model, + async_session=mock_async_session, + ) + + assert exc_info.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert ( + exc_info.value.detail == "Failed to update the Player due to a database error." + ) + mock_retrieve_by_squad_number.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number + ) + mock_update.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number, player_request_model + ) + mock_clear_cache.assert_not_awaited() + + +@pytest.mark.anyio +async def test_request_delete_player_squadnumber_existing_response_no_content( + existing_player_schema, +): + mock_async_session = AsyncMock(spec=AsyncSession) + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number, + patch( + "routes.player_route.player_service.delete_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_delete, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve_by_squad_number.return_value = existing_player_schema + mock_delete.return_value = True + + result = await delete_async( + squad_number=existing_player_schema.squad_number, + async_session=mock_async_session, + ) + + assert result is None + mock_retrieve_by_squad_number.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number + ) + mock_delete.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number + ) + mock_clear_cache.assert_awaited_once_with(CACHE_KEY) + + +@pytest.mark.anyio +async def test_request_delete_player_squadnumber_unknown_response_not_found(): + mock_async_session = AsyncMock(spec=AsyncSession) + unknown_squad_number = 99 + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number, + patch( + "routes.player_route.player_service.delete_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_delete, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve_by_squad_number.return_value = None + + with pytest.raises(HTTPException) as exc_info: + await delete_async( + squad_number=unknown_squad_number, + async_session=mock_async_session, + ) + + assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND + mock_retrieve_by_squad_number.assert_awaited_once_with( + mock_async_session, unknown_squad_number + ) + mock_delete.assert_not_awaited() + mock_clear_cache.assert_not_awaited() + + +@pytest.mark.anyio +async def test_request_delete_player_squadnumber_service_failure_response_server_error( + existing_player_schema, +): + mock_async_session = AsyncMock(spec=AsyncSession) + + with ( + patch( + "routes.player_route.player_service.retrieve_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_retrieve_by_squad_number, + patch( + "routes.player_route.player_service.delete_by_squad_number_async", + new_callable=AsyncMock, + ) as mock_delete, + patch.object( + simple_memory_cache, "clear", new_callable=AsyncMock + ) as mock_clear_cache, + ): + mock_retrieve_by_squad_number.return_value = existing_player_schema + mock_delete.return_value = False + + with pytest.raises(HTTPException) as exc_info: + await delete_async( + squad_number=existing_player_schema.squad_number, + async_session=mock_async_session, + ) + + assert exc_info.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert ( + exc_info.value.detail == "Failed to delete the Player due to a database error." + ) + mock_retrieve_by_squad_number.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number + ) + mock_delete.assert_awaited_once_with( + mock_async_session, existing_player_schema.squad_number + ) + mock_clear_cache.assert_not_awaited() diff --git a/tests/test_player_service.py b/tests/test_player_service.py index a708aa4..326f06f 100644 --- a/tests/test_player_service.py +++ b/tests/test_player_service.py @@ -27,26 +27,17 @@ def existing_player_request_model(): """ Fixture for an existing PlayerRequestModel. """ - existing_player_data = existing_player() return PlayerRequestModel( - first_name=( - existing_player_data.first_name if existing_player_data.first_name else "" - ), - middle_name=existing_player_data.middle_name, - last_name=( - existing_player_data.last_name if existing_player_data.last_name else "" - ), - date_of_birth=existing_player_data.date_of_birth, - squad_number=( - existing_player_data.squad_number - if existing_player_data.squad_number - else 0 - ), - position=existing_player_data.position if existing_player_data.position else "", - abbr_position=existing_player_data.abbr_position, - team=existing_player_data.team, - league=existing_player_data.league, - starting11=existing_player_data.starting11, + first_name="Damián", + middle_name="Emiliano", + last_name="Martínez", + date_of_birth="1992-09-02T00:00:00.000Z", + squad_number=23, + position="Goalkeeper", + abbr_position="GK", + team="Aston Villa FC", + league="Premier League", + starting11=True, ) @@ -55,19 +46,18 @@ def existing_player_schema(): """ Fixture for an existing Player schema. """ - existing_player_data = existing_player() return Player( - id=UUID(existing_player_data.id), - first_name=existing_player_data.first_name, - middle_name=existing_player_data.middle_name, - last_name=existing_player_data.last_name, - date_of_birth=existing_player_data.date_of_birth, - squad_number=existing_player_data.squad_number, - position=existing_player_data.position, - abbr_position=existing_player_data.abbr_position, - team=existing_player_data.team, - league=existing_player_data.league, - starting11=existing_player_data.starting11, + id=UUID("01772c59-43f0-5d85-b913-c78e4e281452"), + first_name="Damián", + middle_name="Emiliano", + last_name="Martínez", + date_of_birth="1992-09-02T00:00:00.000Z", + squad_number=23, + position="Goalkeeper", + abbr_position="GK", + team="Aston Villa FC", + league="Premier League", + starting11=True, ) From 46558e2d51d5a71737db70a3f1b4a5d3f256cb77 Mon Sep 17 00:00:00 2001 From: jishnuc Date: Mon, 28 Sep 2026 20:03:07 +0530 Subject: [PATCH 4/4] test(unit): isolate player unit tests --- CLAUDE.md | 6 ++- tests/unit/conftest.py | 11 ++++ tests/{ => unit}/test_player_route.py | 19 +++++-- tests/{ => unit}/test_player_service.py | 68 +++++++++---------------- 4 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 tests/unit/conftest.py rename tests/{ => unit}/test_player_route.py (95%) rename tests/{ => unit}/test_player_service.py (88%) diff --git a/CLAUDE.md b/CLAUDE.md index a424372..5c21fb1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,9 @@ concerns only; business logic belongs in services. Never skip a layer. - **Line length**: 88; complexity ≤ 10 - **Import order**: stdlib → third-party → local - **Tests**: integration tests against the real SQLite DB (seeded via - Alembic migrations) via `TestClient` — no mocking. Naming pattern + Alembic migrations) via `TestClient` — no mocking. Unit tests in + `tests/unit/` isolate dependencies with mocks and do not require database + migrations. Naming pattern `test_request_{method}_{resource}_{context}_response_{outcome}`; docstrings single-line, concise; `tests/player_fake.py` for test data; `tests/conftest.py` provides a `function`-scoped `client` fixture for @@ -162,6 +164,8 @@ Never suggest a release tag with a coach name not on this list. docstring updates that don't change the API contract) - Tests in `tests/` — maintain async patterns, naming convention, and integration-test approach (no mocking) +- Tests in `tests/unit/` — maintain async patterns, naming convention, and + unit-test isolation (mock dependencies as needed) - Documentation and docstring updates - Lint/format fixes - Refactoring within existing architectural patterns diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..0abff63 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,11 @@ +""" +Fixtures for the unit tests. +""" + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def apply_migrations(): + """Disable database migrations for unit tests.""" + pass diff --git a/tests/test_player_route.py b/tests/unit/test_player_route.py similarity index 95% rename from tests/test_player_route.py rename to tests/unit/test_player_route.py index 9c34e54..0d57c55 100644 --- a/tests/test_player_route.py +++ b/tests/unit/test_player_route.py @@ -75,6 +75,7 @@ def player_schema(): async def test_request_post_player_body_nonexistent_response_created( player_request_model, player_schema ): + """Create a player when the squad number does not already exist.""" mock_async_session = AsyncMock(spec=AsyncSession) response = Response() @@ -116,6 +117,7 @@ async def test_request_post_player_body_nonexistent_response_created( async def test_request_post_player_body_existing_response_conflict( player_request_model, player_schema ): + """Reject player creation when the squad number already exists.""" mock_async_session = AsyncMock(spec=AsyncSession) response = Response() @@ -154,6 +156,7 @@ async def test_request_post_player_body_existing_response_conflict( async def test_request_post_player_body_service_failure_response_server_error( player_request_model, ): + """Raise a server error when player creation fails.""" mock_async_session = AsyncMock(spec=AsyncSession) response = Response() @@ -193,6 +196,7 @@ async def test_request_post_player_body_service_failure_response_server_error( @pytest.mark.anyio async def test_request_get_players_cache_hit_response_success(player_schema): + """Return cached players without querying the service.""" mock_async_session = AsyncMock(spec=AsyncSession) response = Response() @@ -219,6 +223,7 @@ async def test_request_get_players_cache_hit_response_success(player_schema): @pytest.mark.anyio async def test_request_get_players_cache_miss_response_success(player_schema): + """Retrieve and cache players when the cache misses.""" mock_async_session = AsyncMock(spec=AsyncSession) response = Response() @@ -250,6 +255,7 @@ async def test_request_get_players_cache_miss_response_success(player_schema): @pytest.mark.anyio async def test_request_get_player_id_existing_response_success(existing_player_schema): + """Return a player when the requested ID exists.""" mock_async_session = AsyncMock(spec=AsyncSession) with patch( @@ -271,6 +277,7 @@ async def test_request_get_player_id_existing_response_success(existing_player_s @pytest.mark.anyio async def test_request_get_player_id_unknown_response_not_found(): + """Raise not found when the requested ID does not exist.""" mock_async_session = AsyncMock(spec=AsyncSession) unknown_player_id = UUID("00000000-0000-0000-0000-000000000000") @@ -294,6 +301,7 @@ async def test_request_get_player_id_unknown_response_not_found(): async def test_request_get_player_squadnumber_existing_response_success( existing_player_schema, ): + """Return a player when the squad number exists.""" mock_async_session = AsyncMock(spec=AsyncSession) with patch( @@ -315,6 +323,7 @@ async def test_request_get_player_squadnumber_existing_response_success( @pytest.mark.anyio async def test_request_get_player_squadnumber_unknown_response_not_found(): + """Raise not found when the squad number does not exist.""" mock_async_session = AsyncMock(spec=AsyncSession) unknown_squad_number = 99 @@ -340,7 +349,7 @@ async def test_request_get_player_squadnumber_unknown_response_not_found(): async def test_request_put_player_squadnumber_existing_response_no_content( existing_player_schema, player_request_model ): - + """Update a player when the squad number matches.""" player_request_model.squad_number = existing_player_schema.squad_number mock_async_session = AsyncMock(spec=AsyncSession) @@ -380,7 +389,7 @@ async def test_request_put_player_squadnumber_existing_response_no_content( async def test_request_put_player_squadnumber_mismatch_response_bad_request( existing_player_schema, player_request_model ): - + """Reject an update when the URL and body squad numbers differ.""" player_request_model.squad_number = existing_player_schema.squad_number + 1 mock_async_session = AsyncMock(spec=AsyncSession) @@ -416,6 +425,7 @@ async def test_request_put_player_squadnumber_mismatch_response_bad_request( async def test_request_put_player_squadnumber_unknown_response_not_found( player_request_model, ): + """Raise not found when the player does not exist.""" mock_async_session = AsyncMock(spec=AsyncSession) with ( @@ -452,7 +462,7 @@ async def test_request_put_player_squadnumber_unknown_response_not_found( async def test_request_put_player_squadnumber_service_failure_response_server_error( existing_player_schema, player_request_model ): - + """Raise a server error when player update fails.""" player_request_model.squad_number = existing_player_schema.squad_number mock_async_session = AsyncMock(spec=AsyncSession) @@ -496,6 +506,7 @@ async def test_request_put_player_squadnumber_service_failure_response_server_er async def test_request_delete_player_squadnumber_existing_response_no_content( existing_player_schema, ): + """Delete a player when the squad number exists.""" mock_async_session = AsyncMock(spec=AsyncSession) with ( @@ -531,6 +542,7 @@ async def test_request_delete_player_squadnumber_existing_response_no_content( @pytest.mark.anyio async def test_request_delete_player_squadnumber_unknown_response_not_found(): + """Raise not found when the player does not exist.""" mock_async_session = AsyncMock(spec=AsyncSession) unknown_squad_number = 99 @@ -567,6 +579,7 @@ async def test_request_delete_player_squadnumber_unknown_response_not_found(): async def test_request_delete_player_squadnumber_service_failure_response_server_error( existing_player_schema, ): + """Raise a server error when player deletion fails.""" mock_async_session = AsyncMock(spec=AsyncSession) with ( diff --git a/tests/test_player_service.py b/tests/unit/test_player_service.py similarity index 88% rename from tests/test_player_service.py rename to tests/unit/test_player_service.py index 326f06f..0dcc67d 100644 --- a/tests/test_player_service.py +++ b/tests/unit/test_player_service.py @@ -24,9 +24,7 @@ @pytest.fixture def existing_player_request_model(): - """ - Fixture for an existing PlayerRequestModel. - """ + """Fixture for an existing PlayerRequestModel.""" return PlayerRequestModel( first_name="Damián", middle_name="Emiliano", @@ -43,9 +41,7 @@ def existing_player_request_model(): @pytest.fixture def existing_player_schema(): - """ - Fixture for an existing Player schema. - """ + """Fixture for an existing Player schema.""" return Player( id=UUID("01772c59-43f0-5d85-b913-c78e4e281452"), first_name="Damián", @@ -63,9 +59,7 @@ def existing_player_schema(): @pytest.mark.anyio async def test_create_async_success(existing_player_request_model): - """ - Test the successful creation of a Player. - """ + """Test the successful creation of a Player.""" mock_session = AsyncMock(spec=AsyncSession) result = await create_async(mock_session, existing_player_request_model) @@ -78,6 +72,7 @@ async def test_create_async_success(existing_player_request_model): @pytest.mark.anyio async def test_create_async_database_error(existing_player_request_model): + """Return None and roll back when player creation raises a database error.""" mock_session = AsyncMock(spec=AsyncSession) mock_session.commit.side_effect = SQLAlchemyError("Connection timeout") @@ -91,9 +86,7 @@ async def test_create_async_database_error(existing_player_request_model): @pytest.mark.anyio async def test_retrieve_all_async(): - """ - Test the retrieval of all players. - """ + """Test the retrieval of all players.""" mock_session = AsyncMock(spec=AsyncSession) mock_result = Mock() mock_players = [existing_player(), nonexistent_player(), unknown_player()] @@ -109,9 +102,7 @@ async def test_retrieve_all_async(): @pytest.mark.anyio async def test_retrieve_all_async_empty(): - """ - Test the retrieval of all players when no players exist. - """ + """Test the retrieval of all players when no players exist.""" mock_session = AsyncMock(spec=AsyncSession) mock_result = Mock() mock_result.scalars.return_value.all.return_value = [] @@ -126,9 +117,7 @@ async def test_retrieve_all_async_empty(): @pytest.mark.anyio async def test_retrieve_by_id_async(existing_player_schema): - """ - Test the retrieval of a player by ID. - """ + """Test the retrieval of a player by ID.""" mock_session = AsyncMock(spec=AsyncSession) mock_session.get.return_value = existing_player_schema @@ -141,9 +130,7 @@ async def test_retrieve_by_id_async(existing_player_schema): @pytest.mark.anyio async def test_retrieve_by_id_async_not_found(): - """ - Test the retrieval of a player by ID when the player does not exist. - """ + """Test the retrieval of a player by ID when the player does not exist.""" mock_session = AsyncMock(spec=AsyncSession) non_existent_id = UUID("00000000-0000-0000-0000-000000000000") @@ -157,9 +144,7 @@ async def test_retrieve_by_id_async_not_found(): @pytest.mark.anyio async def test_retrieve_by_squad_number_async(existing_player_schema): - """ - Test the retrieval of a player by Squad Number. - """ + """Test the retrieval of a player by Squad Number.""" mock_session = AsyncMock(spec=AsyncSession) mock_result = Mock() mock_result.scalars.return_value.first.return_value = existing_player_schema @@ -173,13 +158,14 @@ async def test_retrieve_by_squad_number_async(existing_player_schema): assert result == existing_player_schema mock_session.execute.assert_awaited_once() + statement = mock_session.execute.await_args.args[0] + expected = Player.squad_number == existing_player_schema.squad_number + assert statement.whereclause.compare(expected) @pytest.mark.anyio async def test_retrieve_by_squad_number_async_not_found(): - """ - Test the retrieval of a player by Squad Number when the player does not exist. - """ + """Test the retrieval of a player by Squad Number when the player does not exist.""" mock_session = AsyncMock(spec=AsyncSession) non_existent_squad_number = 99 @@ -192,16 +178,18 @@ async def test_retrieve_by_squad_number_async_not_found(): ) assert result is None + mock_session.execute.assert_awaited_once() + statement = mock_session.execute.await_args.args[0] + expected = Player.squad_number == non_existent_squad_number + assert statement.whereclause.compare(expected) @pytest.mark.anyio async def test_update_by_squad_number_async_success( existing_player_request_model, existing_player_schema ): - """ - Test the successful update of a player by Squad Number. - """ + """Test the successful update of a player by Squad Number.""" mock_session = AsyncMock(spec=AsyncSession) existing_player_request_model.first_name = "UpdatedFirstName" @@ -259,9 +247,7 @@ async def test_update_by_squad_number_async_success( async def test_update_by_squad_number_async_not_found( existing_player_request_model, ): - """ - Test the update of a player by Squad Number when the player does not exist. - """ + """Test the update of a player by Squad Number when the player does not exist.""" mock_session = AsyncMock(spec=AsyncSession) non_existent_squad_number = 99 @@ -287,9 +273,7 @@ async def test_update_by_squad_number_async_not_found( async def test_update_by_squad_number_async_database_error( existing_player_request_model, existing_player_schema ): - """ - Test the update of a player by Squad Number when a database error occurs. - """ + """Test the update of a player by Squad Number when a database error occurs.""" mock_session = AsyncMock(spec=AsyncSession) with patch( @@ -315,9 +299,7 @@ async def test_update_by_squad_number_async_database_error( @pytest.mark.anyio async def test_delete_by_squad_number_async_success(existing_player_schema): - """ - Test the successful deletion of a player by Squad Number. - """ + """Test the successful deletion of a player by Squad Number.""" mock_session = AsyncMock(spec=AsyncSession) with patch( @@ -342,9 +324,7 @@ async def test_delete_by_squad_number_async_success(existing_player_schema): @pytest.mark.anyio async def test_delete_by_squad_number_async_not_found(): - """ - Test the deletion of a player by Squad Number when the player does not exist. - """ + """Test the deletion of a player by Squad Number when the player does not exist.""" mock_session = AsyncMock(spec=AsyncSession) non_existent_squad_number = 99 @@ -367,9 +347,7 @@ async def test_delete_by_squad_number_async_not_found(): @pytest.mark.anyio async def test_delete_by_squad_number_async_database_error(existing_player_schema): - """ - Test the deletion of a player by Squad Number when a database error occurs. - """ + """Test the deletion of a player by Squad Number when a database error occurs.""" mock_session = AsyncMock(spec=AsyncSession) with patch(