test: Adding unit tests

This commit is contained in:
TrisNol
2023-07-16 11:25:21 +02:00
parent 9da3e4adb9
commit b788ee3659
10 changed files with 4336 additions and 4390 deletions

7828
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,111 +1,112 @@
[build-system] [build-system]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
requires = ["poetry-core"] requires = ["poetry-core"]
[tookl.mypy] [tookl.mypy]
disallow_untyped_defs = true disallow_untyped_defs = true
follow_imports = "silent" follow_imports = "silent"
python_version = "3.11" python_version = "3.11"
warn_redudant_casts = true warn_redudant_casts = true
warn_unused_ignores = true warn_unused_ignores = true
[tool.black] [tool.black]
target-version = ["py311"] target-version = ["py311"]
[tool.coverage.run] [tool.coverage.run]
branch = true branch = true
dynamic_context = "test_function" dynamic_context = "test_function"
relative_files = true relative_files = true
source = ["src"] source = ["src"]
[tool.poetry] [tool.poetry]
authors = ["AKI Projektgruppe 23"] authors = ["AKI Projektgruppe 23"]
description = "A project analysing the german transparenzregister and other data sources to find shared business interests and shared personal and other links for lots of companies." description = "A project analysing the german transparenzregister and other data sources to find shared business interests and shared personal and other links for lots of companies."
name = "aki-prj23-transparenzregister" name = "aki-prj23-transparenzregister"
packages = [{include = "aki_prj23_transparenzregister", from = "src"}] packages = [{include = "aki_prj23_transparenzregister", from = "src"}]
readme = "README.md" readme = "README.md"
version = "0.1.0" version = "0.1.0"
[tool.poetry.dependencies] [tool.poetry.dependencies]
loguru = "^0.7.0" loguru = "^0.7.0"
matplotlib = "^3.7.1" matplotlib = "^3.7.1"
plotly = "^5.14.1" plotly = "^5.14.1"
python = "^3.11" pymongo = "^4.4.1"
seaborn = "^0.12.2" python = "^3.11"
selenium = "^4.10.0" seaborn = "^0.12.2"
tqdm = "^4.65.0" selenium = "^4.10.0"
tqdm = "^4.65.0"
[tool.poetry.group.develop.dependencies]
black = {extras = ["jupyter"], version = "^23.3.0"} [tool.poetry.group.develop.dependencies]
jupyterlab = "^4.0.0" black = {extras = ["jupyter"], version = "^23.3.0"}
nbconvert = "^7.4.0" jupyterlab = "^4.0.0"
pre-commit = "^3.3.2" nbconvert = "^7.4.0"
rise = "^5.7.1" pre-commit = "^3.3.2"
rise = "^5.7.1"
[tool.poetry.group.doc.dependencies]
jupyter = "^1.0.0" [tool.poetry.group.doc.dependencies]
myst-parser = "^1.0.0" jupyter = "^1.0.0"
nbsphinx = "^0.9.2" myst-parser = "^1.0.0"
sphinx = "^6.0.0" nbsphinx = "^0.9.2"
sphinx-copybutton = "^0.5.2" sphinx = "^6.0.0"
sphinx-rtd-theme = "^1.2.1" sphinx-copybutton = "^0.5.2"
sphinx_autodoc_typehints = "*" sphinx-rtd-theme = "^1.2.1"
sphinxcontrib-mermaid = "^0.9.2" sphinx_autodoc_typehints = "*"
sphinxcontrib-napoleon = "^0.7" sphinxcontrib-mermaid = "^0.9.2"
sphinxcontrib-napoleon = "^0.7"
[tool.poetry.group.lint.dependencies]
black = "^23.3.0" [tool.poetry.group.lint.dependencies]
mypy = "^1.3.0" black = "^23.3.0"
pandas-stubs = "^2.0.1.230501" mypy = "^1.3.0"
ruff = "^0.0.270" pandas-stubs = "^2.0.1.230501"
types-requests = "^2.31.0.1" ruff = "^0.0.270"
types-requests = "^2.31.0.1"
[tool.poetry.group.test.dependencies]
pytest = "^7.3.1" [tool.poetry.group.test.dependencies]
pytest-clarity = "^1.0.1" pytest = "^7.3.1"
pytest-cov = "^4.1.0" pytest-clarity = "^1.0.1"
pytest-mock = "^3.10.0" pytest-cov = "^4.1.0"
pytest-repeat = "^0.9.1" pytest-mock = "^3.10.0"
pytest-repeat = "^0.9.1"
[tool.ruff]
exclude = [ [tool.ruff]
".bzr", exclude = [
".direnv", ".bzr",
".eggs", ".direnv",
".git", ".eggs",
".git-rewrite", ".git",
".hg", ".git-rewrite",
".mypy_cache", ".hg",
".nox", ".mypy_cache",
".pants.d", ".nox",
".pytype", ".pants.d",
".ruff_cache", ".pytype",
".svn", ".ruff_cache",
".tox", ".svn",
".venv", ".tox",
"__pypackages__", ".venv",
"_build", "__pypackages__",
"buck-out", "_build",
"build", "buck-out",
"dist", "build",
"node_modules", "dist",
"venv" "node_modules",
] "venv"
# Never enforce `E501` (line length violations). ]
ignore = ["E501"] # Never enforce `E501` (line length violations).
line-length = 88 ignore = ["E501"]
# Enable flake8-bugbear (`B`) rules. line-length = 88
select = ["E", "F", "B", "I", "S", "RSE", "RET", "SLF", "SIM", "TID", "PD", "PL", "PLE", "PLR", "PLW", "NPY", "UP", "D", "N", "A", "C4", "T20", "PT"] # Enable flake8-bugbear (`B`) rules.
src = ["src"] select = ["E", "F", "B", "I", "S", "RSE", "RET", "SLF", "SIM", "TID", "PD", "PL", "PLE", "PLR", "PLW", "NPY", "UP", "D", "N", "A", "C4", "T20", "PT"]
target-version = "py311" src = ["src"]
# Avoid trying to fix flake8-bugbear (`B`) violations. target-version = "py311"
unfixable = ["B"] # Avoid trying to fix flake8-bugbear (`B`) violations.
unfixable = ["B"]
[tool.ruff.flake8-builtins]
builtins-ignorelist = ["id"] [tool.ruff.flake8-builtins]
builtins-ignorelist = ["id"]
[tool.ruff.per-file-ignores]
"tests/*.py" = ["S101"] [tool.ruff.per-file-ignores]
"tests/*.py" = ["S101", "D100", "D101", "D107", "D103"]
[tool.ruff.pydocstyle]
convention = "google" [tool.ruff.pydocstyle]
convention = "google"

View File

@ -1,54 +1,49 @@
"""CompanyMongoService.""" """CompanyMongoService."""
from models.company import Company from aki_prj23_transparenzregister.models.company import Company, CompanyID
from utils.company_service_interface import CompanyServiceInterface from aki_prj23_transparenzregister.utils.mongo import MongoConnector
from utils.mongo import MongoConnector
class CompanyMongoService:
class CompanyMongoService(CompanyServiceInterface): """_summary_."""
"""_summary_.
def __init__(self, connector: MongoConnector):
Args: """_summary_.
CompanyServiceInterface (_type_): _description_
""" Args:
connector (MongoConnector): _description_
def __init__(self, connector: MongoConnector): """
"""_summary_. self.collection = connector.database["companies"]
Args: def get_all(self) -> list[Company]:
connector (MongoConnector): _description_ """_summary_.
"""
self.collection = connector.database["companies"] Returns:
list[Company]: _description_
def get_all(self) -> list[Company]: """
"""_summary_. result = self.collection.find()
return list(result)
Returns:
list[Company]: _description_ def get_by_id(self, id: CompanyID) -> Company | None:
""" """_summary_.
result = self.collection.find()
return list(result) Args:
id (str): _description_
def get_by_id(self, id: str) -> Company | None:
"""_summary_. Returns:
Company | None: _description_
Args: """
id (str): _description_ result = list(self.collection.find({"id": id}))
if len(result) == 1:
Returns: return result[0]
Company | None: _description_ return None
"""
result = list(self.collection.find({"id": id})) def insert(self, company: Company):
if len(result) == 1: """_summary_.
return result[0]
return None Args:
company (Company): _description_
def insert(self, company: Company):
"""_summary_. Returns:
_type_: _description_
Args: """
company (Company): _description_ return self.collection.insert_one(company.to_dict())
Returns:
_type_: _description_
"""
return self.collection.insert_one(company.to_dict())

View File

@ -1,51 +0,0 @@
"""CompanyServiceInterface."""
import abc
from models import Company
class CompanyServiceInterface(abc.ABC):
"""Generic abstract interface for CRUD operations of a Company.
Args:
ABC (_type_): Abstract class
"""
@abc.abstractmethod
def get_all(self) -> list[Company.Company]:
"""_summary_.
Raises:
NotImplementedError: _description_
Returns:
list[Company.Company]: _description_
"""
raise NotImplementedError
@abc.abstractmethod
def get_by_id(self, id: Company.CompayID) -> Company.Company | None:
"""_summary_.
Args:
id (Company.CompayID): _description_
Raises:
NotImplementedError: _description_
Returns:
Company.Company | None: _description_
"""
raise NotImplementedError
@abc.abstractmethod
def insert(self, company: Company.Company):
"""_summary_.
Args:
company (Company.Company): _description_
Raises:
NotImplementedError: _description_
"""
raise NotImplementedError

View File

@ -1,155 +1,60 @@
"""Mongo Wrapper.""" """Mongo Wrapper."""
from dataclasses import dataclass from dataclasses import dataclass
import pymongo import pymongo
from models.news import News
from utils.news_service_interface import (
NewsServiceInterface, @dataclass
) class MongoConnection:
"""_summary_."""
@dataclass hostname: str
class MongoConnection: database: str
"""_summary_.""" port: int | None
username: str | None
hostname: str password: str | None
database: str
port: int | None
username: str | None class MongoConnector:
password: str | None """Wrapper for establishing a connection to a MongoDB instance."""
def __init__(self, connection: MongoConnection):
class MongoConnector: """_summary_.
"""Wrapper for establishing a connection to a MongoDB instance."""
Args:
def __init__(self, connection: MongoConnection): connection (MongoConnection): Wrapper for connection string
"""_summary_. """
self.client = self.connect(
Args: connection.hostname,
connection (MongoConnection): Wrapper for connection string connection.port,
""" connection.username,
self.client = self.connect( connection.password,
connection.hostname, )
connection.port, self.database = self.client[connection.database]
connection.username,
connection.password, def connect(
) self,
self.database = self.client[connection.database] hostname: str,
port: int | None,
def connect( username: str | None,
self, password: str | None,
hostname: str, ) -> pymongo.MongoClient:
port: int | None, """_summary_.
username: str | None,
password: str | None, Args:
) -> pymongo.MongoClient: hostname (str): hostname
"""_summary_. port (int): port
username (str): Username
Args: password (str): Password
hostname (str): hostname
port (int): port Returns:
username (str): Username pymongo.MongoClient: MongoClient connect to the DB
password (str): Password """
if username is not None and password is not None:
Returns: connection_string = f"mongodb+srv://{username}:{password}@{hostname}"
pymongo.MongoClient: MongoClient connect to the DB else:
""" connection_string = f"mongodb+srv://{hostname}"
if username is not None and password is not None: if port is not None:
connection_string = f"mongodb+srv://{username}:{password}@{hostname}" connection_string += f":{port}"
else: connection_string = connection_string.replace("mongodb+srv", "mongodb")
connection_string = f"mongodb+srv://{hostname}" return pymongo.MongoClient(connection_string)
if port is not None:
connection_string += f":{port}"
connection_string = connection_string.replace("mongodb+srv", "mongodb")
return pymongo.MongoClient(connection_string)
class MongoNewsService(NewsServiceInterface):
"""_summary_.
Args:
NewsServiceInterface (_type_): _description_
"""
def __init__(self, connector: MongoConnector):
"""_summary_.
Args:
connector (MongoConnector): _description_
"""
self.collection = connector.database["news"]
def get_all(self) -> list[News]:
"""_summary_.
Returns:
list[News]: _description_
"""
result = self.collection.find()
return [MongoEntryTransformer.transform_outgoing(elem) for elem in result]
def get_by_id(self, id: str) -> News | None:
"""_summary_.
Args:
id (str): _description_
Returns:
News | None: _description_
"""
result = list(self.collection.find({"_id": id}))
if len(result) == 1:
return MongoEntryTransformer.transform_outgoing(list(result)[0])
return None
def insert(self, news: News):
"""_summary_.
Args:
news (News): _description_
Returns:
_type_: _description_
"""
return self.collection.insert_one(MongoEntryTransformer.transform_ingoing(news))
class MongoEntryTransformer:
"""_summary_.
Returns:
_type_: _description_
"""
@staticmethod
def transform_ingoing(news: News) -> dict:
"""Convert a News object to a dictionary compatible with a MongoDB entry.
Args:
news (News): News object to be transformed
Returns:
dict: Transformed data with added _id field
"""
transport_object = news.to_dict()
transport_object["_id"] = news.id
del transport_object["id"]
return transport_object
@staticmethod
def transform_outgoing(data: dict) -> News:
"""Reverse the transform_ingoing method.
Args:
data (dict): dict from the MongoDB to be transformed
Returns:
News: News entry based on MongoDB document
"""
return News(
id=data["_id"],
title=data["title"],
date=data["date"],
text=data["text"],
source_url=data["source_url"],
)

View File

@ -0,0 +1,94 @@
"""MongoNewsService."""
from aki_prj23_transparenzregister.models.news import News
from aki_prj23_transparenzregister.utils.mongo import MongoConnector
class MongoNewsService:
"""_summary_.
Args:
NewsServiceInterface (_type_): _description_
"""
def __init__(self, connector: MongoConnector):
"""_summary_.
Args:
connector (MongoConnector): _description_
"""
self.collection = connector.database["news"]
def get_all(self) -> list[News]:
"""_summary_.
Returns:
list[News]: _description_
"""
result = self.collection.find()
return [MongoEntryTransformer.transform_outgoing(elem) for elem in result]
def get_by_id(self, id: str) -> News | None:
"""_summary_.
Args:
id (str): _description_
Returns:
News | None: _description_
"""
result = list(self.collection.find({"_id": id}))
if len(result) == 1:
return MongoEntryTransformer.transform_outgoing(list(result)[0])
return None
def insert(self, news: News):
"""_summary_.
Args:
news (News): _description_
Returns:
_type_: _description_
"""
return self.collection.insert_one(MongoEntryTransformer.transform_ingoing(news))
class MongoEntryTransformer:
"""_summary_.
Returns:
_type_: _description_
"""
@staticmethod
def transform_ingoing(news: News) -> dict:
"""Convert a News object to a dictionary compatible with a MongoDB entry.
Args:
news (News): News object to be transformed
Returns:
dict: Transformed data with added _id field
"""
transport_object = news.to_dict()
transport_object["_id"] = news.id
del transport_object["id"]
return transport_object
@staticmethod
def transform_outgoing(data: dict) -> News:
"""Reverse the transform_ingoing method.
Args:
data (dict): dict from the MongoDB to be transformed
Returns:
News: News entry based on MongoDB document
"""
return News(
id=data["_id"],
title=data["title"],
date=data["date"],
text=data["text"],
source_url=data["source_url"],
)

View File

@ -1,63 +0,0 @@
"""NewsServiceInterface."""
import abc
from models.news import News
class NewsServiceInterface(abc.ABC):
"""Generic abstract interface for a NewsService handling CRUD operations.
Args:
ABC (_type_): Abstract class
"""
@abc.abstractmethod
def get_all(self) -> list[News]:
"""Get a list of all News articles.
Raises:
NotImplementedError: To be defined by child classes
Returns:
list[News]: Results
"""
raise NotImplementedError
@abc.abstractmethod
def get_by_id(self, id: str) -> News | None:
"""Get an entry by an ID.
Args:
id (str): ID identifying the entry
Raises:
NotImplementedError: To be defined by child classes
Returns:
News | None: Found object or None if no entry with ID found
"""
raise NotImplementedError
@abc.abstractmethod
def insert(self, news: News):
"""Insert a News entry into the DB.
Args:
news (News): News object to be saved
Raises:
NotImplementedError: To be defined by child classes
"""
raise NotImplementedError
@abc.abstractmethod
def insert_many(self, news: list[News]):
"""Inserts many documents at once.
Args:
news (list[News]): List of News entries to be saved
Raises:
NotImplementedError: To be defined by child classes
"""
raise NotImplementedError

View File

@ -1,4 +1,4 @@
"""Test Models.nes.""" """Test Models.nesws."""
from aki_prj23_transparenzregister.models.news import News from aki_prj23_transparenzregister.models.news import News

View File

@ -0,0 +1,103 @@
"""Test utils.company_mongo_service."""
from unittest.mock import Mock
import pytest
from aki_prj23_transparenzregister.models.company import Company
from aki_prj23_transparenzregister.utils.company_mongo_service import (
CompanyMongoService,
)
@pytest.fixture()
def mock_mongo_connector(mocker) -> Mock:
"""Mock MongoConnector class.
Args:
mocker (any): Library mocker
Returns:
Mock: Mocked MongoConnector
"""
mock = Mock()
mocker.patch(
"aki_prj23_transparenzregister.utils.mongo.MongoConnector", return_value=mock
)
return mock
@pytest.fixture()
def mock_collection() -> Mock:
"""Mock mongo collection.
Returns:
Mock: Mock object
"""
return Mock()
def test_init(mock_mongo_connector, mock_collection):
"""Test CompanyMongoService constructor.
Args:
mock_mongo_connector (Mock): Mocked MongoConnector library
mock_collection (Mock): Mocked pymongo collection
"""
mock_mongo_connector.database = {"companies": mock_collection}
service = CompanyMongoService(mock_mongo_connector)
assert service.collection == mock_collection
def test_get_all(mock_mongo_connector, mock_collection):
"""Test CompanyMongoService get_all method.
Args:
mock_mongo_connector (Mock): Mocked MongoConnector library
mock_collection (Mock): Mocked pymongo collection
"""
mock_mongo_connector.database = {"companies": mock_collection}
service = CompanyMongoService(mock_mongo_connector)
mock_result = [{"id": "42"}]
mock_collection.find.return_value = mock_result
assert service.get_all() == mock_result
def test_by_id_no_result(mock_mongo_connector, mock_collection):
"""Test CompanyMongoService get_by_id with no result.
Args:
mock_mongo_connector (Mock): Mocked MongoConnector library
mock_collection (Mock): Mocked pymongo collection
"""
mock_mongo_connector.database = {"companies": mock_collection}
service = CompanyMongoService(mock_mongo_connector)
mock_collection.find.return_value = []
assert service.get_by_id("Does not exist") is None
def test_by_id_result(mock_mongo_connector, mock_collection):
"""Test CompanyMongoService get_by_id with result.
Args:
mock_mongo_connector (Mock): Mocked MongoConnector library
mock_collection (Mock): Mocked pymongo collection
"""
mock_mongo_connector.database = {"companies": mock_collection}
service = CompanyMongoService(mock_mongo_connector)
mock_entry = {"id": "Does exist", "vaue": 42}
mock_collection.find.return_value = [mock_entry]
assert service.get_by_id("Does exist") == mock_entry
def test_insert(mock_mongo_connector, mock_collection):
"""Test CompanyMongoService insert method.
Args:
mock_mongo_connector (Mock): Mocked MongoConnector library
mock_collection (Mock): Mocked pymongo collection
"""
mock_mongo_connector.database = {"companies": mock_collection}
service = CompanyMongoService(mock_mongo_connector)
mock_result = 42
mock_collection.insert_one.return_value = mock_result
assert service.insert(Company(None, None, "", "", [])) == mock_result

View File

@ -0,0 +1,44 @@
from unittest.mock import Mock
import pytest
from aki_prj23_transparenzregister.utils.news_mongo_service import MongoNewsService
@pytest.fixture()
def mock_mongo_connector(mocker) -> Mock:
"""Mock MongoConnector class.
Args:
mocker (any): Library mocker
Returns:
Mock: Mocked MongoConnector
"""
mock = Mock()
mocker.patch(
"aki_prj23_transparenzregister.utils.mongo.MongoConnector", return_value=mock
)
return mock
@pytest.fixture()
def mock_collection() -> Mock:
"""Mock mongo collection.
Returns:
Mock: Mock object
"""
return Mock()
def test_init(mock_mongo_connector, mock_collection):
"""Test CompanyMongoService constructor.
Args:
mock_mongo_connector (Mock): Mocked MongoConnector library
mock_collection (Mock): Mocked pymongo collection
"""
mock_mongo_connector.database = {"news": mock_collection}
service = MongoNewsService(mock_mongo_connector)
assert service.collection == mock_collection