Compare commits
6
Commits
0948799653
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eafd73743 | ||
|
|
1cc047d3b8 | ||
|
|
c7cc2a3655 | ||
|
|
b5d0297375 | ||
|
|
2fa633a44f | ||
|
|
0e47db238a |
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
env:
|
||||
UV_LINK_MODE: copy
|
||||
run: uv sync
|
||||
run: uv sync --extra redis
|
||||
|
||||
- name: Run pytest
|
||||
env:
|
||||
|
||||
@@ -53,6 +53,10 @@ show_error_codes = true # show error codes in output
|
||||
explicit_package_bases = true # reduce risk of import confusion
|
||||
namespace_packages = true # enable namespace packages
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "testcontainers.*"
|
||||
ignore_missing_imports = true
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"mypy>=1.17.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Integration tests configuration."""
|
||||
|
||||
import os
|
||||
from typing import Generator
|
||||
from collections.abc import Generator
|
||||
import pytest
|
||||
import redis
|
||||
import structlog
|
||||
@@ -27,15 +27,12 @@ def configure_logging() -> None:
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def redis_container() -> Generator[str, None, None]:
|
||||
def redis_container() -> Generator[str]:
|
||||
"""Set up a Redis container for testing and yield the Redis URI."""
|
||||
# Start container
|
||||
container = RedisContainer(
|
||||
image="redis/redis-stack:7.2.0-v0",
|
||||
port=6379,
|
||||
).with_bind_ports(
|
||||
container=6379,
|
||||
host=6379,
|
||||
)
|
||||
container.start()
|
||||
# Set environment variable for Redis URI
|
||||
@@ -52,7 +49,7 @@ def redis_container() -> Generator[str, None, None]:
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def set_environment_variables(
|
||||
redis_container: str,
|
||||
) -> Generator[dict[str, str], None, None]:
|
||||
) -> Generator[dict[str, str]]:
|
||||
"""Set environment variables needed for tests."""
|
||||
# Build environment variables dictionary
|
||||
env_vars = {"REDIS_URI": redis_container}
|
||||
@@ -68,7 +65,7 @@ def set_environment_variables(
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def raw_redis_client(redis_container: str) -> Generator[redis.Redis, None, None]:
|
||||
def raw_redis_client(redis_container: str) -> Generator[redis.Redis]:
|
||||
"""Provide a raw Redis client connected to the test Redis container."""
|
||||
# Connect client
|
||||
client = redis.Redis.from_url(
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"""Integration tests for ConnectionAwareInterface."""
|
||||
|
||||
import pytest
|
||||
from python_repositories.interfaces.connection_aware_interface import ConnectionAwareInterface
|
||||
from python_repositories.interfaces.connection_aware_interface import (
|
||||
ConnectionAwareInterface,
|
||||
)
|
||||
|
||||
|
||||
def test_instantiation_fails_when_connect_not_implemented() -> None:
|
||||
"""Test that instantiation fails if connect is not implemented."""
|
||||
|
||||
class Incomplete(ConnectionAwareInterface):
|
||||
"""A class that does not implement connect."""
|
||||
|
||||
def disconnect(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -16,13 +20,15 @@ def test_instantiation_fails_when_connect_not_implemented() -> None:
|
||||
return False
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
_ = Incomplete()
|
||||
_ = Incomplete() # type: ignore
|
||||
|
||||
|
||||
def test_instantiation_fails_when_disconnect_not_implemented() -> None:
|
||||
"""Test that instantiation fails if disconnect is not implemented."""
|
||||
|
||||
class Incomplete(ConnectionAwareInterface):
|
||||
"""A class that does not implement disconnect."""
|
||||
|
||||
def connect(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -31,13 +37,15 @@ def test_instantiation_fails_when_disconnect_not_implemented() -> None:
|
||||
return False
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
_ = Incomplete()
|
||||
_ = Incomplete() # type: ignore
|
||||
|
||||
|
||||
def test_instantiation_fails_when_is_connected_not_implemented() -> None:
|
||||
"""Test that instantiation fails if is_connected is not implemented."""
|
||||
|
||||
class Incomplete(ConnectionAwareInterface):
|
||||
"""A class that does not implement is_connected."""
|
||||
|
||||
def connect(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -45,15 +53,17 @@ def test_instantiation_fails_when_is_connected_not_implemented() -> None:
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
_ = Incomplete()
|
||||
_ = Incomplete() # type: ignore
|
||||
|
||||
|
||||
def test_connect_raises_not_implemented_if_not_overwritten() -> None:
|
||||
"""Test that connect raises NotImplementedError if not implemented."""
|
||||
|
||||
class Incomplete(ConnectionAwareInterface):
|
||||
"""A class that does not implement connect."""
|
||||
|
||||
def connect(self) -> None:
|
||||
super().connect()
|
||||
super().connect() # type: ignore
|
||||
|
||||
def disconnect(self) -> None:
|
||||
pass
|
||||
@@ -69,13 +79,15 @@ def test_connect_raises_not_implemented_if_not_overwritten() -> None:
|
||||
|
||||
def test_disconnect_raises_not_implemented_if_not_overwritten() -> None:
|
||||
"""Test that disconnect raises NotImplementedError if not implemented."""
|
||||
|
||||
class Incomplete(ConnectionAwareInterface):
|
||||
"""A class that does not implement disconnect."""
|
||||
|
||||
def connect(self) -> None:
|
||||
pass
|
||||
|
||||
def disconnect(self) -> None:
|
||||
super().disconnect()
|
||||
super().disconnect() # type: ignore
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
@@ -88,8 +100,10 @@ def test_disconnect_raises_not_implemented_if_not_overwritten() -> None:
|
||||
|
||||
def test_is_connected_raises_not_implemented_if_not_overwritten() -> None:
|
||||
"""Test that is_connected raises NotImplementedError if not implemented."""
|
||||
|
||||
class Incomplete(ConnectionAwareInterface):
|
||||
"""A class that does not implement is_connected."""
|
||||
|
||||
def connect(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -98,7 +112,7 @@ def test_is_connected_raises_not_implemented_if_not_overwritten() -> None:
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return super().is_connected
|
||||
return super().is_connected # type: ignore
|
||||
|
||||
instance = Incomplete()
|
||||
with pytest.raises(NotImplementedError):
|
||||
|
||||
@@ -1,39 +1,47 @@
|
||||
"""Integration tests for ContextAwareInterface."""
|
||||
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
from python_repositories.interfaces.context_aware_interface import ContextAwareInterface
|
||||
|
||||
|
||||
def test_instantiation_fails_when_enter_not_implemented() -> None:
|
||||
"""Test that instantiation fails if __enter__ is not implemented."""
|
||||
|
||||
class Incomplete(ContextAwareInterface):
|
||||
"""A class that does not implement __enter__."""
|
||||
|
||||
def __exit__(
|
||||
self, exc_type: type | None, exc_val: object | None, exc_tb: object | None
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
_ = Incomplete()
|
||||
_ = Incomplete() # type: ignore
|
||||
|
||||
|
||||
def test_instantiation_fails_when_exit_not_implemented() -> None:
|
||||
"""Test that instantiation fails if __exit__ is not implemented."""
|
||||
|
||||
class Incomplete(ContextAwareInterface):
|
||||
"""A class that does not implement __exit__."""
|
||||
def __enter__(self) -> ContextAwareInterface:
|
||||
|
||||
def __enter__(self) -> Incomplete:
|
||||
return self
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
_ = Incomplete()
|
||||
_ = Incomplete() # type: ignore
|
||||
|
||||
|
||||
def test_enter_raises_not_implemented_if_not_overwritten() -> None:
|
||||
"""Test that __enter__ raises NotImplementedError if not implemented."""
|
||||
|
||||
class Incomplete(ContextAwareInterface):
|
||||
"""A class that does not implement __enter__."""
|
||||
def __enter__(self) -> ContextAwareInterface:
|
||||
super().__enter__()
|
||||
|
||||
def __enter__(self) -> Incomplete:
|
||||
super().__enter__() # type: ignore
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self, exc_type: type | None, exc_val: object | None, exc_tb: object | None
|
||||
@@ -47,15 +55,17 @@ def test_enter_raises_not_implemented_if_not_overwritten() -> None:
|
||||
|
||||
def test_exit_raises_not_implemented_if_not_overwritten() -> None:
|
||||
"""Test that __exit__ raises NotImplementedError if not implemented."""
|
||||
|
||||
class Incomplete(ContextAwareInterface):
|
||||
"""A class that does not implement __exit__."""
|
||||
|
||||
def __enter__(self) -> ContextAwareInterface:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self, exc_type: type | None, exc_val: object | None, exc_tb: object | None
|
||||
) -> None:
|
||||
super().__exit__(exc_type, exc_val, exc_tb)
|
||||
super().__exit__(exc_type, exc_val, exc_tb) # type: ignore
|
||||
|
||||
instance = Incomplete()
|
||||
with pytest.raises(NotImplementedError):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# allowing tests to access RedisAdapter's internal methods as needed for integration testing.
|
||||
"""Integration tests for the RedisAdapter."""
|
||||
|
||||
from typing import Generator
|
||||
from collections.abc import Generator
|
||||
import pytest
|
||||
import redis
|
||||
from redis.commands.json.path import Path as RedisPath
|
||||
@@ -11,7 +11,7 @@ from python_repositories.adapters.redis_adapter import RedisAdapter
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def data() -> Generator[dict[str, str], None, None]:
|
||||
def data() -> Generator[dict[str, str]]:
|
||||
"""Provide a sample data dictionary for tests."""
|
||||
yield {"foo": "bar"}
|
||||
|
||||
@@ -20,7 +20,7 @@ def data() -> Generator[dict[str, str], None, None]:
|
||||
def data_in_redis(
|
||||
raw_redis_client: redis.Redis,
|
||||
data: dict[str, str],
|
||||
) -> Generator[tuple[str, dict[str, str]], None, None]:
|
||||
) -> Generator[tuple[str, dict[str, str]]]:
|
||||
"""Fixture to set up a known value in Redis before each test."""
|
||||
key = "test_key"
|
||||
path = RedisPath.root_path()
|
||||
@@ -33,7 +33,7 @@ def data_in_redis(
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def redis_adapter(redis_container: str) -> Generator[RedisAdapter, None, None]:
|
||||
def redis_adapter(redis_container: str) -> Generator[RedisAdapter]:
|
||||
"""Fixture to provide a connected RedisAdapter instance."""
|
||||
adapter = RedisAdapter()
|
||||
adapter.connect()
|
||||
@@ -48,26 +48,20 @@ def clear_redis(raw_redis_client: redis.Redis) -> None:
|
||||
raw_redis_client.flushall()
|
||||
|
||||
|
||||
def test_should_adhere_to_interface(
|
||||
redis_container: str
|
||||
) -> None:
|
||||
def test_should_adhere_to_interface(redis_container: str) -> None:
|
||||
"""Test that the RedisAdapter adheres to the expected interface."""
|
||||
# Instantiation fails if interface not adhered to
|
||||
_ = RedisAdapter()
|
||||
|
||||
|
||||
def test_should_have_logger_when_instantiated(
|
||||
redis_container: str
|
||||
) -> None:
|
||||
def test_should_have_logger_when_instantiated(redis_container: str) -> None:
|
||||
"""Test that the RedisAdapter has a logger when instantiated."""
|
||||
adapter = RedisAdapter()
|
||||
assert hasattr(adapter, "logger")
|
||||
assert adapter.logger is not None
|
||||
|
||||
|
||||
def test_should_not_be_connected_when_instantiated(
|
||||
redis_container: str
|
||||
) -> None:
|
||||
def test_should_not_be_connected_when_instantiated(redis_container: str) -> None:
|
||||
"""Test that the RedisAdapter is not connected when instantiated."""
|
||||
adapter = RedisAdapter()
|
||||
assert adapter._client is None
|
||||
@@ -89,7 +83,7 @@ def test_should_raise_connection_error_when_unable_to_connect(
|
||||
|
||||
|
||||
def test_connect_raises_connection_error_when_unable_to_ping(
|
||||
monkeypatch: pytest.MonkeyPatch
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that the RedisAdapter raises ConnectionError when ping fails."""
|
||||
# Set an invalid URI
|
||||
@@ -98,6 +92,7 @@ def test_connect_raises_connection_error_when_unable_to_ping(
|
||||
# Monkeypatch redis.Redis.from_url to return a mock client
|
||||
class MockRedis:
|
||||
"""A mock Redis client that simulates a failed ping."""
|
||||
|
||||
def ping(self) -> bool:
|
||||
"""Simulate a failed ping."""
|
||||
return False # Simulate failed ping
|
||||
@@ -109,23 +104,6 @@ def test_connect_raises_connection_error_when_unable_to_ping(
|
||||
adapter.connect()
|
||||
|
||||
|
||||
def test_should_connect_and_disconnect(
|
||||
redis_container: str,
|
||||
) -> None:
|
||||
"""Test that the RedisAdapter can connect and disconnect."""
|
||||
# Connect
|
||||
adapter = RedisAdapter()
|
||||
adapter.connect()
|
||||
# Assert connected
|
||||
assert adapter._client is not None
|
||||
assert adapter.is_connected
|
||||
# Disconnect
|
||||
adapter.disconnect()
|
||||
# Assert disconnected
|
||||
assert adapter._client is None
|
||||
assert not adapter.is_connected
|
||||
|
||||
|
||||
def test_should_log_error_on_exception_during_exit(
|
||||
redis_container: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -142,9 +120,7 @@ def test_should_log_error_on_exception_during_exit(
|
||||
assert "Error while exiting context" in caplog.text
|
||||
|
||||
|
||||
def test_should_have_context_manager(
|
||||
redis_container: str
|
||||
) -> None:
|
||||
def test_should_have_context_manager(redis_container: str) -> None:
|
||||
"""Test that the RedisAdapter can be used as a context manager."""
|
||||
with RedisAdapter() as adapter:
|
||||
assert adapter._client is not None
|
||||
@@ -183,7 +159,7 @@ def test_should_raise_value_error_on_invalid_get_key(
|
||||
# Act & Assert
|
||||
for key in invalid_keys:
|
||||
with pytest.raises(ValueError):
|
||||
redis_adapter._get(key)
|
||||
redis_adapter._get(key) # type: ignore
|
||||
|
||||
|
||||
def test_should_raise_connection_error_on_get_when_not_connected(
|
||||
@@ -234,7 +210,7 @@ def test_should_raise_value_error_on_invalid_set_key(
|
||||
# Act & Assert
|
||||
for key in invalid_keys:
|
||||
with pytest.raises(ValueError):
|
||||
redis_adapter._set(key, data)
|
||||
redis_adapter._set(key, data) # type: ignore
|
||||
|
||||
|
||||
def test_should_raise_value_error_on_invalid_set_data(
|
||||
@@ -247,7 +223,7 @@ def test_should_raise_value_error_on_invalid_set_data(
|
||||
# Act & Assert
|
||||
for data in invalid_data:
|
||||
with pytest.raises(ValueError):
|
||||
redis_adapter._set(key, data)
|
||||
redis_adapter._set(key, data) # type: ignore
|
||||
|
||||
|
||||
def test_should_raise_connection_error_on_set_when_not_connected(
|
||||
@@ -285,7 +261,7 @@ def test_should_raise_value_error_on_invalid_delete_key(
|
||||
# Act & Assert
|
||||
for key in invalid_keys:
|
||||
with pytest.raises(ValueError):
|
||||
redis_adapter._delete(key)
|
||||
redis_adapter._delete(key) # type: ignore
|
||||
|
||||
|
||||
def test_should_raise_connection_error_on_delete_when_not_connected(
|
||||
@@ -321,7 +297,7 @@ def test_should_raise_value_error_on_invalid_list_keys_pattern(
|
||||
# Act & Assert
|
||||
for pattern in invalid_patterns:
|
||||
with pytest.raises(ValueError):
|
||||
redis_adapter._list_keys(pattern)
|
||||
redis_adapter._list_keys(pattern) # type: ignore
|
||||
|
||||
|
||||
def test_should_raise_connection_error_on_list_keys_when_not_connected(
|
||||
|
||||
Reference in New Issue
Block a user