Skip to content

Commit

Permalink
chore(tests): add project TestCase (#69)
Browse files Browse the repository at this point in the history
Add a project `TestCase` that extends `unittest.TestCase` to provide the following:

- Configure the execution environment at the class level, once for the whole `TestCase`. This is similar to calling `app.setup`.
- Add a class method, `config_factory` that returns an `app.lib.Config` instance used to configure the execution environment of this `TestCase` instances.

All existing unit tests have also been refactored to extend from this TestCase. Included also are minor bug fixes and improvements.
  • Loading branch information
kennedykori committed Apr 14, 2023
1 parent f2a7a3c commit c0d249f
Show file tree
Hide file tree
Showing 21 changed files with 109 additions and 54 deletions.
32 changes: 17 additions & 15 deletions .config.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# allows the app to communicate with the remote IDR Server and for data
# exchange to happen.
# This is a required setting.
DEFAULT_TRANSPORT_FACTORY: "app.lib.transports.http.http_transport_factory"
DEFAULT_TRANSPORT_FACTORY: app.lib.transports.http.http_transport_factory

# The logging configuration for the app. This app use Python's built it logging.
# https://docs.python.org/3/library/logging.html
Expand All @@ -25,12 +25,14 @@ LOGGING:
format: "%(levelname)s: %(asctime)s %(module)s %(process)d %(thread)d %(message)s"
handlers:
console:
level: "DEBUG"
class: "logging.StreamHandler"
formatter: "verbose"
root:
level: "INFO"
handlers": [ "console" ]
level: DEBUG
class: logging.StreamHandler
formatter: verbose
loggers:
app:
level: INFO
handlers:
- console

# The retry module global configuration. The values here will be used for each
# retry instance when not specified.
Expand All @@ -47,7 +49,7 @@ RETRY:
# on that data including what is uploaded to the server.
# This setting is not required and defaults to an empty list.
#SUPPORTED_DATA_SOURCE_TYPES:
# - "app.imp.sql_data.SQLDataSourceType"
# - app.imp.sql_data.SQLDataSourceType



Expand All @@ -58,13 +60,13 @@ RETRY:
# Configuration specific to the HTTPTransport.
# This is only required if HTTPTransport is set as the default Transport.
HTTP_TRANSPORT:
default_http_api_dialect_factory: "app.lib.transports.http.idr_server_api_v1_dialect_factory"
default_http_api_dialect_factory: app.lib.transports.http.idr_server_api_v1_dialect_factory
connect_timeout: 60 # 1 minutes
read_timeout: 60 # 1 minute

# A function that when called should return a HTTPAPIDialect.
# This is only required if HTTPTransport is set as the default Transport.
DEFAULT_HTTP_API_DIALECT_FACTORY: "app.lib.http.idr_server_api_v1_dialect_factory"
DEFAULT_HTTP_API_DIALECT_FACTORY: app.lib.http.idr_server_api_v1_dialect_factory

# The configuration of the backend to this app. This setting is used by HTTP
# API dialects.
Expand All @@ -84,10 +86,10 @@ REMOTE_SERVER:
# MySQL databases.
# This is only required if the SQLDataSourceType is in use.
MYSQL_DB_INSTANCE:
host: "localhost"
host: localhost
port: 3306
username: "idr_client"
password: "securePa$$word"
username: idr_client
password: securePa$$word



Expand All @@ -100,6 +102,6 @@ MYSQL_DB_INSTANCE:
# location data is composed of a unique code for ease of indexing and a
# human-readable name.
# Both of these settings are required.
ORG_UNIT_CODE: "12345"
ORG_UNIT_CODE: 12345

ORG_UNIT_NAME: "Test Facility"
ORG_UNIT_NAME: Test Facility
13 changes: 11 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"verbose": {
"format": (
"%(levelname)s: %(asctime)s %(module)s "
"%(process)d %(thread)d %(message)s"
"%(process)d %(message)s"
),
},
},
Expand All @@ -48,7 +48,9 @@
"formatter": "verbose",
},
},
"root": {"level": "INFO", "handlers": ["console"]},
"loggers": {
"app": {"level": "INFO", "handlers": ["console"]},
},
},
_SETTINGS_INITIALIZERS_CONFIG_KEY: [],
_SUPPORTED_DATA_SOURCE_TYPES_CONFIG_KEY: [],
Expand Down Expand Up @@ -231,6 +233,11 @@ def setup(
"""
Set up the application and ready it for use.
This involves configuring the settings and app registry, configuring
logging, loading supported
:class:`data source types <app.core.DataSourceType>` and initializing the
default :class:`transport <app.core.Transport>`.
:param initial_settings: Optional configuration parameters to override the
defaults.
:param settings_initializers:
Expand All @@ -255,6 +262,8 @@ def setup(
_settings_dict.get(_SETTINGS_INITIALIZERS_CONFIG_KEY, ()),
),
)
# FIXME: This is hardcoded default behaviour and it's problematic to test
# or mock properly.
_initializers.insert(0, _LoggingInitializer())
_initializers.insert(1, _SupportedDataSourceTypesInitializer())
_initializers.insert(2, _DefaultTransportFactoryInitializer())
Expand Down
1 change: 1 addition & 0 deletions app/lib/transports/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,6 @@ def http_transport_factory() -> HTTPTransport:
"HTTPAPIDialect",
"HTTPTransport",
"IDRServerAPIv1",
"http_transport_factory",
"idr_server_api_v1_dialect_factory",
]
56 changes: 56 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from unittest import TestCase as _TestCase
from unittest.mock import patch

from app.lib import AppRegistry, Config

from .factories import config_factory as _cf


class TestCase(_TestCase):
"""A project-specific ``TestCase``.
Extend the default :class:`unittest.TestCase` to provide the following:
* Configure the execution environment at the class level, once for the
whole ``TestCase``. This is similar to calling :func:`app.setup`.
* Add a class method, :meth:`config_factory` that returns a
:class:`Config <app.lib.Config>` instance used to configure the
execution environment of this ``TestCase`` instances.
"""

@classmethod
def config_factory(cls) -> Config:
"""Return a ``Config`` instance.
Return a :class:`Config <app.lib.Config>` instance used to configure
the execution environment of this ``TestCase`` instances.
:return: a Config instance.
"""
return Config(settings=_cf())

@classmethod
def setUpClass(cls) -> None:
"""Set up class fixture before running the tests in the class.
Extend the default implementation by setting up a patch for the
application settings and registry using the value returned by the
:meth:`config_factory` class method. The patch is accessible through
the class attribute :attr:`config_patch` for interested subclasses.
.. warning::
It is highly encouraged that subclasses extending this method call
``super()`` to avoid weird, unpredictable behaviour during test
runs caused by misconfigured execution environments.
:return: None.
"""
super().setUpClass()
config_patcher = patch.multiple(
"app",
registry=AppRegistry(),
settings=cls.config_factory(),
)
config_patcher.start()
cls.config_patcher = config_patcher
cls.addClassCleanup(config_patcher.stop)
3 changes: 1 addition & 2 deletions tests/core/test_domain.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from unittest import TestCase

import pytest

from app.core import AbstractDomainObject, IdentifiableDomainObject
from tests import TestCase

from .factories import (
FakeDataSource,
Expand Down
3 changes: 1 addition & 2 deletions tests/core/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from unittest import TestCase

from app.core import IDRClientException
from tests import TestCase


class TestIDRClientException(TestCase):
Expand Down
2 changes: 1 addition & 1 deletion tests/imp/sql_data/test_domain.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
from typing import TYPE_CHECKING, Any
from unittest import TestCase
from unittest.mock import patch

import pytest
Expand All @@ -18,6 +17,7 @@
SupportedDBVendors,
)
from app.lib import Config, ImproperlyConfiguredError
from tests import TestCase

from .factories import (
SQLDataSourceFactory,
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/config/test_config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import TYPE_CHECKING, Any
from unittest import TestCase

import pytest

from app.lib import Config, MissingSettingError, SettingInitializer
from tests import TestCase

if TYPE_CHECKING:
from collections.abc import Mapping, Sequence
Expand Down
7 changes: 2 additions & 5 deletions tests/lib/retry/test_retry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime, timedelta
from unittest import TestCase
from unittest.mock import MagicMock, patch

import pytest
Expand All @@ -12,6 +11,7 @@
if_exception_type_factory,
if_idr_exception,
)
from tests import TestCase


def test_if_idr_exception() -> None:
Expand Down Expand Up @@ -54,10 +54,7 @@ def setUp(self) -> None:
self._patch.start()
self._instance: Retry = Retry()
self._instance.load_config()

def tearDown(self) -> None:
super().tearDown()
self._patch.stop()
self.addCleanup(self._patch.stop)

def test_calculate_deadline_time_return_value_with_no_deadline(
self,
Expand Down
5 changes: 2 additions & 3 deletions tests/lib/retry/test_setting_initializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from unittest import TestCase

import pytest

from app.lib import ImproperlyConfiguredError, RetryInitializer
from app.lib.retry.constants import DEFAULT_RETRY_CONFIG
from tests import TestCase


class TestRetryInitializer(TestCase):
Expand Down Expand Up @@ -41,6 +40,6 @@ def test_execute_when_invalid_config_is_provided(self) -> None:
self._instance.execute({setting: "-0.1"}) # type: ignore

def test_setting_property_return_value(self) -> None:
"""Assert the the ``setting`` property returns the expected value."""
"""Assert the ``setting`` property returns the expected value."""

assert self._instance.setting == "RETRY"
2 changes: 1 addition & 1 deletion tests/lib/tasks/test_common.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import TYPE_CHECKING
from unittest import TestCase

import pytest

from app.core import Task
from app.lib import Chainable, Consumer, Pipeline
from tests import TestCase

if TYPE_CHECKING:
from collections.abc import Callable
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/tasks/test_concurrent.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from concurrent.futures import Executor, ThreadPoolExecutor, wait
from typing import TYPE_CHECKING
from unittest import TestCase

import pytest

from app.core import Task
from app.lib import ConcurrentExecutor, completed_successfully
from app.lib.tasks.concurrent import ConcurrentExecutorDisposedError
from tests import TestCase

if TYPE_CHECKING:
from collections.abc import Sequence
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/tasks/test_sql.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import TYPE_CHECKING
from unittest import TestCase
from unittest.mock import patch

import pytest
Expand All @@ -8,6 +7,7 @@

from app.core import ExtractionOperationError
from app.lib import SimpleSQLSelect
from tests import TestCase

if TYPE_CHECKING:
from pandas import DataFrame
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/test_app_registry.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import TYPE_CHECKING
from unittest import TestCase

import pytest

from app.imp.sql_data import SQLDataSourceType
from app.lib import AppRegistry, ImproperlyConfiguredError
from app.lib.transports.http import http_transport_factory
from tests import TestCase

if TYPE_CHECKING:
from collections.abc import Mapping
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/transports/http/test_api_v1_dialect.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json
from typing import TYPE_CHECKING, Any
from unittest import TestCase
from unittest.mock import patch

import pytest
Expand All @@ -10,6 +9,7 @@
IDRServerAPIv1,
idr_server_api_v1_dialect_factory,
)
from tests import TestCase
from tests.core.factories import (
FakeDataSourceFactory,
FakeDataSourceTypeFactory,
Expand Down
9 changes: 4 additions & 5 deletions tests/lib/transports/http/test_http.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from typing import Any
from unittest import TestCase
from unittest.mock import patch

import pytest

import app
from app.lib import ImproperlyConfiguredError
from app.lib.transports.http import HTTPTransport, http_transport_factory
from tests import TestCase
from tests.factories import config_factory


Expand All @@ -32,10 +31,10 @@ def test_http_transport_factory_with_valid_settings_works(self) -> None:
Assert that a http transport factory returns the expected value with
a valid config.
"""
app.setup(initial_settings=self._app_config)
transport: HTTPTransport = http_transport_factory()
with patch("app.settings", self._app_config):
transport: HTTPTransport = http_transport_factory()

assert transport is not None
assert transport is not None

def test_http_transport_factory_with_invalid_settings_fails(self) -> None:
"""
Expand Down
6 changes: 3 additions & 3 deletions tests/lib/transports/http/test_http_transport.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from unittest import TestCase
from unittest.mock import patch

import pytest
from requests.exceptions import ChunkedEncodingError, ConnectionError

from app.core import TransportClosedError, TransportError
from app.lib.transports.http import HTTPAPIDialect, HTTPTransport
from tests import TestCase
from tests.core.factories import (
FakeDataSourceFactory,
FakeDataSourceTypeFactory,
Expand Down Expand Up @@ -63,8 +63,8 @@ def test_dispose_returns_cleanly(self) -> None:

def test_a_disposed_transport_raises_expected_errors(self) -> None:
"""
Assert that a disposed transport raises ``TransportClosedError`` on
attempted usage.
Assert that a disposed Transport instance raises
``TransportClosedError`` on attempted usage.
"""
data_source = FakeDataSourceFactory()
data_source_type = FakeDataSourceTypeFactory()
Expand Down

0 comments on commit c0d249f

Please sign in to comment.