Skip to content

Commit

Permalink
test(coverage): add test infrastructure and coverage config
Browse files Browse the repository at this point in the history
- Add test infrastructure with pytest configuration and fixtures
- Configure code coverage settings in .coveragerc
- Move StrEnum from typing.py to models.py
- Add Makefile with common development tasks
  • Loading branch information
bendikrb committed Dec 3, 2024
1 parent 4cccd0a commit 45ebc57
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 37 deletions.
13 changes: 13 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[run]
branch = True
omit =
kassalappy/cli.py

[report]
exclude_lines =
pragma: no cover
def __repr__
def __str__
def __rich_console__
if __name__ == .__main__.:
if TYPE_CHECKING
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
run := poetry run

.PHONY: test
test:
$(run) pytest tests/ $(ARGS)

.PHONY: test-coverage
test-coverage:
$(run) pytest tests/ --cov-report term-missing --cov=kassalappy $(ARGS)

.PHONY: coverage
coverage:
$(run) coverage html

.PHONY: format
format:
$(run) ruff format kassalappy

.PHONY: format-check
format-check:
$(run) ruff --check kassalappy

.PHONY: setup
setup:
poetry install

.PHONY: update
update:
poetry update

.PHONY: repl
repl:
$(run) python

5 changes: 3 additions & 2 deletions kassalappy/const.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Constants used by kassalapp."""

from http import HTTPStatus
from importlib.metadata import version
from typing import Final

VERSION = version("kassalappy")
from .version import __version__

VERSION = __version__

API_ENDPOINT: Final = "https://kassal.app/api/v1"
DEFAULT_TIMEOUT: Final = 10
Expand Down
31 changes: 29 additions & 2 deletions kassalappy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from abc import ABC
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
import logging
from typing import ClassVar, Literal

Expand All @@ -11,10 +12,36 @@
from mashumaro.mixins.orjson import DataClassORJSONMixin
from typing_extensions import TypedDict

from .typing import StrEnum

_LOGGER = logging.getLogger()


# noinspection PyUnresolvedReferences
class StrEnum(str, Enum):
"""A string enumeration of type `(str, Enum)`.
All members are compared via `upper()`. Defaults to UNKNOWN.
"""

def __str__(self) -> str:
return str(self.value)

def __eq__(self, other: str) -> bool:
other = other.upper()
return super().__eq__(other)

@classmethod
def _missing_(cls, value) -> str:
has_unknown = False
for member in cls:
if member.name.upper() == "UNKNOWN":
has_unknown = True
if member.name.upper() == value.upper():
return member
if has_unknown:
_LOGGER.warning("'%s' is not a valid '%s'", value, cls.__name__)
return cls.UNKNOWN
raise ValueError(f"'{value}' is not a valid {cls.__name__}")


Unit = Literal[
"cl",
"cm",
Expand Down
33 changes: 0 additions & 33 deletions kassalappy/typing.py

This file was deleted.

5 changes: 5 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""kassalappy tests."""

import pytest

pytestmark = pytest.mark.asyncio(loop_scope="package")
31 changes: 31 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

import logging
from typing import Callable

from aiohttp import ClientSession

import pytest

from kassalappy import Kassalapp

_LOGGER = logging.getLogger(__name__)


@pytest.fixture
async def kassalapp_client(default_access_token) -> Callable[..., Kassalapp]:
"""Return Politikontroller Client."""

def _kassalapp_client(
access_token: str | None = None,
session: ClientSession | None = None,
) -> Kassalapp:
token = access_token if access_token is not None else default_access_token
return Kassalapp(access_token=token, websession=session)

return _kassalapp_client


@pytest.fixture
def default_access_token():
return "baba"
1 change: 1 addition & 0 deletions tests/fixtures/health.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"data":{"status":"ok"}}
21 changes: 21 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from pathlib import Path

import orjson

FIXTURE_DIR = Path(__file__).parent / "fixtures"


def load_fixture(name: str) -> str:
"""Load a fixture."""
path = FIXTURE_DIR / f"{name}.json"
if not path.exists(): # pragma: no cover
raise FileNotFoundError(f"Fixture {name} not found")
return path.read_text(encoding="utf-8")


def load_fixture_json(name: str) -> dict | list:
"""Load a fixture as JSON."""
data = load_fixture(name)
return orjson.loads(data)
17 changes: 17 additions & 0 deletions tests/ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This extends our general Ruff rules specifically for tests
extend = "../pyproject.toml"

lint.extend-select = [
"PT", # Use @pytest.fixture without parentheses
]

lint.extend-ignore = [
"S101",
"S106",
"S108",
"SLF001",
"TCH002",
"PLR2004",
]

lint.pylint.max-branches = 13
25 changes: 25 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Tests for NrkPodcastAPI."""

from __future__ import annotations

import logging

from aiohttp import ClientSession
from aiohttp.web_response import json_response
from aresponses import Response, ResponsesMockServer

from kassalappy.models import StatusResponse
from .helpers import load_fixture_json

logger = logging.getLogger(__name__)


async def test_health(aresponses: ResponsesMockServer, kassalapp_client):
aresponses.add(
response=json_response(load_fixture_json("health")),
)
async with ClientSession() as session:
client = kassalapp_client(session=session)
result = await client.healthy()
assert isinstance(result, StatusResponse)
assert result.status == "ok"

0 comments on commit 45ebc57

Please sign in to comment.