"""
crate_anon/testing/classes.py
===============================================================================
Copyright (C) 2015, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CRATE.
CRATE is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CRATE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CRATE. If not, see <https://www.gnu.org/licenses/>.
===============================================================================
Test classes for more complex tests e.g. where a database session is required.
"""
import logging
from typing import Generator, TYPE_CHECKING
from unittest import TestCase
from faker import Faker
import pytest
from sqlalchemy.engine.base import Engine
from crate_anon.testing.providers import register_all_providers
from crate_anon.testing.factories import (
AnonTestBaseFactory,
SecretBaseFactory,
SourceTestBaseFactory,
set_sqlalchemy_session_on_all_factories,
)
if TYPE_CHECKING:
from sqlalchemy.orm.session import Session
[docs]class CrateTestCase(TestCase):
[docs] def setUp(self) -> None:
super().setUp()
self.fake = Faker("en_GB")
register_all_providers(self.fake)
def assert_logged(
self,
logger_name: str,
level: int,
expected_message: str,
logging_cm: Generator[None, None, None],
) -> None:
level_name = logging.getLevelName(level)
search = f"{level_name}:{logger_name}:{expected_message}"
self.assertTrue(
any(search in line for line in logging_cm.output),
msg=f"Failed to find '{search}' in {logging_cm.output}",
)
[docs]class CommonDatabaseTestCase(CrateTestCase):
"""
Base class for testing with a database. Do not inherit from this directly,
use one of the below subclasses instead, which will be associated with
pytest fixtures for engine, session etc.
"""
anon_dbsession: "Session"
secret_dbsession: "Session"
source_dbsession: "Session"
anon_engine: Engine
secret_engine: Engine
source_engine: Engine
databases_on_disk: bool
anon_db_filename: str
secret_db_filename: str
source_db_filename: str
[docs] def setUp(self) -> None:
set_sqlalchemy_session_on_all_factories(
AnonTestBaseFactory, self.anon_dbsession
)
set_sqlalchemy_session_on_all_factories(
SecretBaseFactory, self.secret_dbsession
)
set_sqlalchemy_session_on_all_factories(
SourceTestBaseFactory, self.source_dbsession
)
[docs] def set_echo(self, echo: bool) -> None:
"""
Changes the database echo status.
"""
self.anon_engine.echo = echo
self.secret_engine.echo = echo
self.source_engine.echo = echo
[docs]@pytest.mark.usefixtures("setup")
class DatabaseTestCase(CommonDatabaseTestCase):
"""
Base class for testing with a database.
The pytest fixtures defined in conftest.py run each test in a transaction,
rolling back the transaction at the end of the test. This all works fine,
unless one of the tests encounters a DatabaseError and the transaction
needs to be rolled back. In this case we need the approach taken by
SlowSecretDatabaseTestCase below.
"""
[docs]@pytest.mark.usefixtures("slow_secret_setup")
class SlowSecretDatabaseTestCase(CommonDatabaseTestCase):
"""
Like DatabaseTestCase but we create and drop all of the tables for the
secret database every time a test is run. Potentially slow if there are
lots of tables.
"""
[docs]class DemoDatabaseTestCase(DatabaseTestCase):
"""
Base class for use with test factories such as
DemoPatientFactory
"""