Skip to content

Providers API Reference

django_testcontainers_plus.providers.base.ContainerProvider

Bases: ABC

Base class for all container providers.

Each provider is responsible for: 1. Detecting if the service is needed from Django settings 2. Creating and configuring the container 3. Providing settings updates with container connection info

Source code in src/django_testcontainers_plus/providers/base.py
class ContainerProvider(ABC):
    """Base class for all container providers.

    Each provider is responsible for:
    1. Detecting if the service is needed from Django settings
    2. Creating and configuring the container
    3. Providing settings updates with container connection info
    """

    @property
    @abstractmethod
    def name(self) -> str:
        """Unique identifier for this provider."""
        ...

    @abstractmethod
    def can_auto_detect(self, settings: Any, context: dict[str, Any] | None = None) -> bool:
        """Check if this service is needed based on Django settings.

        Args:
            settings: Django settings module
            context: Optional dict with pre-test-setup values that Django's test
                framework may have overwritten.

        Returns:
            True if this service should be automatically started
        """
        ...

    @abstractmethod
    def get_container(self, config: dict[str, Any]) -> DockerContainer:
        """Create and configure the container.

        Args:
            config: Configuration dict from TESTCONTAINERS setting

        Returns:
            Configured testcontainer instance
        """
        ...

    @abstractmethod
    def update_settings(
        self, container: DockerContainer, settings: Any, config: dict[str, Any]
    ) -> dict[str, Any]:
        """Generate settings updates with container connection info.

        Args:
            container: Running container instance
            settings: Django settings module
            config: Configuration dict from TESTCONTAINERS setting

        Returns:
            Dict of settings updates to apply
        """
        ...

    def get_default_config(self) -> dict[str, Any]:
        """Get default configuration for this provider.

        Returns:
            Default configuration dict
        """
        return {}

name abstractmethod property

Unique identifier for this provider.

can_auto_detect(settings, context=None) abstractmethod

Check if this service is needed based on Django settings.

Parameters:

Name Type Description Default
settings Any

Django settings module

required
context dict[str, Any] | None

Optional dict with pre-test-setup values that Django's test framework may have overwritten.

None

Returns:

Type Description
bool

True if this service should be automatically started

Source code in src/django_testcontainers_plus/providers/base.py
@abstractmethod
def can_auto_detect(self, settings: Any, context: dict[str, Any] | None = None) -> bool:
    """Check if this service is needed based on Django settings.

    Args:
        settings: Django settings module
        context: Optional dict with pre-test-setup values that Django's test
            framework may have overwritten.

    Returns:
        True if this service should be automatically started
    """
    ...

get_container(config) abstractmethod

Create and configure the container.

Parameters:

Name Type Description Default
config dict[str, Any]

Configuration dict from TESTCONTAINERS setting

required

Returns:

Type Description
DockerContainer

Configured testcontainer instance

Source code in src/django_testcontainers_plus/providers/base.py
@abstractmethod
def get_container(self, config: dict[str, Any]) -> DockerContainer:
    """Create and configure the container.

    Args:
        config: Configuration dict from TESTCONTAINERS setting

    Returns:
        Configured testcontainer instance
    """
    ...

update_settings(container, settings, config) abstractmethod

Generate settings updates with container connection info.

Parameters:

Name Type Description Default
container DockerContainer

Running container instance

required
settings Any

Django settings module

required
config dict[str, Any]

Configuration dict from TESTCONTAINERS setting

required

Returns:

Type Description
dict[str, Any]

Dict of settings updates to apply

Source code in src/django_testcontainers_plus/providers/base.py
@abstractmethod
def update_settings(
    self, container: DockerContainer, settings: Any, config: dict[str, Any]
) -> dict[str, Any]:
    """Generate settings updates with container connection info.

    Args:
        container: Running container instance
        settings: Django settings module
        config: Configuration dict from TESTCONTAINERS setting

    Returns:
        Dict of settings updates to apply
    """
    ...

get_default_config()

Get default configuration for this provider.

Returns:

Type Description
dict[str, Any]

Default configuration dict

Source code in src/django_testcontainers_plus/providers/base.py
def get_default_config(self) -> dict[str, Any]:
    """Get default configuration for this provider.

    Returns:
        Default configuration dict
    """
    return {}

django_testcontainers_plus.manager.ContainerManager

Manages lifecycle of test containers.

Source code in src/django_testcontainers_plus/manager.py
class ContainerManager:
    """Manages lifecycle of test containers."""

    def __init__(self, settings: Any, context: dict[str, Any] | None = None):
        """Initialize container manager.

        Args:
            settings: Django settings module
            context: Optional dict with pre-test-setup values that Django's test
                framework may have overwritten. Passed to providers during
                auto-detection.
        """
        self.settings = settings
        self.context = context
        self.providers: list[ContainerProvider] = PROVIDER_REGISTRY
        self.active_containers: dict[str, DockerContainer] = {}
        self.settings_updates: dict[str, Any] = {}

    def get_testcontainers_config(self) -> dict[str, Any]:
        """Get TESTCONTAINERS configuration from settings.

        Returns:
            Configuration dict, empty if not defined
        """
        return getattr(self.settings, "TESTCONTAINERS", {})

    def detect_needed_containers(self) -> list[ContainerProvider]:
        """Detect which containers are needed based on settings.

        Returns:
            List of providers that should be started

        Raises:
            MissingDependencyError: If a needed provider is unavailable
        """
        config = self.get_testcontainers_config()
        needed_providers = []

        for provider in self.providers:
            provider_config = config.get(provider.name, {})

            if "enabled" in provider_config:
                if provider_config["enabled"]:
                    needed_providers.append(provider)
                continue

            if provider_config.get("auto", True) is False:
                continue

            if provider.can_auto_detect(self.settings, self.context):
                needed_providers.append(provider)

        for provider_name in config.keys():
            provider_config = config[provider_name]
            if provider_config.get("enabled", True):
                found_provider: ContainerProvider | None = next(
                    (p for p in self.providers if p.name == provider_name), None
                )
                if found_provider is not None and found_provider not in needed_providers:
                    needed_providers.append(found_provider)

        self._check_unavailable_providers()

        return needed_providers

    def start_containers(self) -> dict[str, Any]:
        """Start all needed containers.

        Returns:
            Dict of settings updates to apply
        """
        needed_providers = self.detect_needed_containers()
        config = self.get_testcontainers_config()
        all_updates: dict[str, Any] = {}

        for provider in needed_providers:
            provider_config = {
                **provider.get_default_config(),
                **config.get(provider.name, {}),
            }

            container = provider.get_container(provider_config)
            container.start()

            self.active_containers[provider.name] = container

            updates = provider.update_settings(container, self.settings, provider_config)

            self._merge_updates(all_updates, updates)

        self.settings_updates = all_updates
        return all_updates

    def stop_containers(self) -> None:
        """Stop and remove all active containers."""
        for container in self.active_containers.values():
            try:
                container.stop()
            except Exception:
                ...

        self.active_containers.clear()

    def _merge_updates(self, target: dict[str, Any], updates: dict[str, Any]) -> None:
        """Deep merge settings updates.

        Args:
            target: Target dict to merge into
            updates: Updates to merge
        """
        for key, value in updates.items():
            if key in target and isinstance(target[key], dict) and isinstance(value, dict):
                self._merge_updates(target[key], value)
            else:
                target[key] = value

    def _check_unavailable_providers(self) -> None:
        """Check if any unavailable providers would have been auto-detected.

        Raises:
            MissingDependencyError: If a provider is needed but unavailable
        """
        if not UNAVAILABLE_PROVIDERS:
            return

        config = self.get_testcontainers_config()

        for provider_name, (
            extra_name,
            original_error,
        ) in UNAVAILABLE_PROVIDERS.items():
            provider_config = config.get(provider_name, {})

            if provider_config.get("enabled", False):
                self._raise_missing_dependency_error(
                    provider_name,
                    extra_name,
                    original_error,
                    f"TESTCONTAINERS['{provider_name}']",
                )

            if provider_config.get("auto", True) is not False:
                detected_location = self._would_be_auto_detected(provider_name)
                if detected_location:
                    self._raise_missing_dependency_error(
                        provider_name, extra_name, original_error, detected_location
                    )

    def _would_be_auto_detected(self, provider_name: str) -> str | None:
        """Check if a provider would be auto-detected from settings.

        Args:
            provider_name: Name of the provider to check

        Returns:
            String describing where it was detected, or None if not detected
        """
        if provider_name == "mysql":
            databases = getattr(self.settings, "DATABASES", {})
            for db_name, db_config in databases.items():
                if isinstance(db_config, dict):
                    engine = db_config.get("ENGINE", "")
                    if "mysql" in engine.lower() or "mariadb" in engine.lower():
                        return f"DATABASES['{db_name}']['ENGINE']"

        elif provider_name == "redis":
            caches = getattr(self.settings, "CACHES", {})
            for cache_name, cache_config in caches.items():
                if isinstance(cache_config, dict):
                    backend = cache_config.get("BACKEND", "")
                    if "redis" in backend.lower():
                        return f"CACHES['{cache_name}']['BACKEND']"

            celery_broker = getattr(self.settings, "CELERY_BROKER_URL", "")
            if "redis://" in celery_broker.lower():
                return "CELERY_BROKER_URL"

            session_engine = getattr(self.settings, "SESSION_ENGINE", "")
            if "redis" in session_engine.lower():
                return "SESSION_ENGINE"

        elif provider_name == "s3":
            storages = getattr(self.settings, "STORAGES", {})
            if isinstance(storages, dict):
                for storage_name, storage_config in storages.items():
                    if isinstance(storage_config, dict):
                        backend = str(storage_config.get("BACKEND", "")).lower()
                        if "s3boto3" in backend or "storages.backends.s3." in backend:
                            return f"STORAGES['{storage_name}']['BACKEND']"

            default_storage = str(getattr(self.settings, "DEFAULT_FILE_STORAGE", "")).lower()
            if "s3boto3" in default_storage or "storages.backends.s3." in default_storage:
                return "DEFAULT_FILE_STORAGE"

            bucket_name = getattr(self.settings, "AWS_STORAGE_BUCKET_NAME", "")
            if bucket_name:
                return "AWS_STORAGE_BUCKET_NAME"

        return None

    def _raise_missing_dependency_error(
        self,
        provider_name: str,
        extra_name: str,
        original_error: Exception,
        detected_in: str,
    ) -> None:
        """Raise a helpful MissingDependencyError.

        Args:
            provider_name: Name of the provider
            extra_name: Name of the pip extra
            original_error: The original import error
            detected_in: Where the provider was detected
        """
        # Capitalize provider name for display
        display_name = provider_name.upper() if provider_name == "mysql" else provider_name.title()

        raise MissingDependencyError(
            provider_name=display_name,
            extra_name=extra_name,
            detected_in=detected_in,
            original_error=original_error,
        )

__init__(settings, context=None)

Initialize container manager.

Parameters:

Name Type Description Default
settings Any

Django settings module

required
context dict[str, Any] | None

Optional dict with pre-test-setup values that Django's test framework may have overwritten. Passed to providers during auto-detection.

None
Source code in src/django_testcontainers_plus/manager.py
def __init__(self, settings: Any, context: dict[str, Any] | None = None):
    """Initialize container manager.

    Args:
        settings: Django settings module
        context: Optional dict with pre-test-setup values that Django's test
            framework may have overwritten. Passed to providers during
            auto-detection.
    """
    self.settings = settings
    self.context = context
    self.providers: list[ContainerProvider] = PROVIDER_REGISTRY
    self.active_containers: dict[str, DockerContainer] = {}
    self.settings_updates: dict[str, Any] = {}

get_testcontainers_config()

Get TESTCONTAINERS configuration from settings.

Returns:

Type Description
dict[str, Any]

Configuration dict, empty if not defined

Source code in src/django_testcontainers_plus/manager.py
def get_testcontainers_config(self) -> dict[str, Any]:
    """Get TESTCONTAINERS configuration from settings.

    Returns:
        Configuration dict, empty if not defined
    """
    return getattr(self.settings, "TESTCONTAINERS", {})

detect_needed_containers()

Detect which containers are needed based on settings.

Returns:

Type Description
list[ContainerProvider]

List of providers that should be started

Raises:

Type Description
MissingDependencyError

If a needed provider is unavailable

Source code in src/django_testcontainers_plus/manager.py
def detect_needed_containers(self) -> list[ContainerProvider]:
    """Detect which containers are needed based on settings.

    Returns:
        List of providers that should be started

    Raises:
        MissingDependencyError: If a needed provider is unavailable
    """
    config = self.get_testcontainers_config()
    needed_providers = []

    for provider in self.providers:
        provider_config = config.get(provider.name, {})

        if "enabled" in provider_config:
            if provider_config["enabled"]:
                needed_providers.append(provider)
            continue

        if provider_config.get("auto", True) is False:
            continue

        if provider.can_auto_detect(self.settings, self.context):
            needed_providers.append(provider)

    for provider_name in config.keys():
        provider_config = config[provider_name]
        if provider_config.get("enabled", True):
            found_provider: ContainerProvider | None = next(
                (p for p in self.providers if p.name == provider_name), None
            )
            if found_provider is not None and found_provider not in needed_providers:
                needed_providers.append(found_provider)

    self._check_unavailable_providers()

    return needed_providers

start_containers()

Start all needed containers.

Returns:

Type Description
dict[str, Any]

Dict of settings updates to apply

Source code in src/django_testcontainers_plus/manager.py
def start_containers(self) -> dict[str, Any]:
    """Start all needed containers.

    Returns:
        Dict of settings updates to apply
    """
    needed_providers = self.detect_needed_containers()
    config = self.get_testcontainers_config()
    all_updates: dict[str, Any] = {}

    for provider in needed_providers:
        provider_config = {
            **provider.get_default_config(),
            **config.get(provider.name, {}),
        }

        container = provider.get_container(provider_config)
        container.start()

        self.active_containers[provider.name] = container

        updates = provider.update_settings(container, self.settings, provider_config)

        self._merge_updates(all_updates, updates)

    self.settings_updates = all_updates
    return all_updates

stop_containers()

Stop and remove all active containers.

Source code in src/django_testcontainers_plus/manager.py
def stop_containers(self) -> None:
    """Stop and remove all active containers."""
    for container in self.active_containers.values():
        try:
            container.stop()
        except Exception:
            ...

    self.active_containers.clear()

django_testcontainers_plus.runner.TestcontainersRunner

Bases: DiscoverRunner

Django test runner that automatically manages testcontainers.

This runner extends Django's DiscoverRunner to automatically: 1. Detect needed containers from settings 2. Start containers before test databases are set up 3. Update database settings with container connection info 4. Clean up containers after tests complete

Usage

settings.py

TEST_RUNNER = 'django_testcontainers_plus.runner.TestcontainersRunner'

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'test', } }

Source code in src/django_testcontainers_plus/runner.py
class TestcontainersRunner(DiscoverRunner):
    """Django test runner that automatically manages testcontainers.

    This runner extends Django's DiscoverRunner to automatically:
    1. Detect needed containers from settings
    2. Start containers before test databases are set up
    3. Update database settings with container connection info
    4. Clean up containers after tests complete

    Usage:
        # settings.py
        TEST_RUNNER = 'django_testcontainers_plus.runner.TestcontainersRunner'

        DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.postgresql',
                'NAME': 'test',
            }
        }
    """

    def __init__(self, *args: Any, **kwargs: Any):
        """Initialize the test runner."""
        super().__init__(*args, **kwargs)
        self.container_manager: ContainerManager | None = None
        self.original_settings: dict[str, Any] = {}

    def setup_test_environment(self, **kwargs: Any) -> None:
        """Set up test environment and start containers."""
        # Capture original settings before Django's setup_test_environment
        # overwrites them (e.g. EMAIL_BACKEND is set to locmem)
        context = {
            "original_email_backend": getattr(settings, "EMAIL_BACKEND", None),
        }

        super().setup_test_environment(**kwargs)

        self.container_manager = ContainerManager(settings, context=context)

        settings_updates = self.container_manager.start_containers()

        if settings_updates:
            apply_settings_updates(settings_updates, self.original_settings)
            recreate_database_connections()

        if self.verbosity >= 1:
            for provider_name in self.container_manager.active_containers.keys():
                print(f"Started {provider_name} container for testing")

    def teardown_test_environment(self, **kwargs: Any) -> None:
        """Tear down test environment and stop containers."""
        restore_settings(self.original_settings)

        if self.container_manager:
            if self.verbosity >= 1:
                print("Stopping test containers...")
            self.container_manager.stop_containers()

        super().teardown_test_environment(**kwargs)

__init__(*args, **kwargs)

Initialize the test runner.

Source code in src/django_testcontainers_plus/runner.py
def __init__(self, *args: Any, **kwargs: Any):
    """Initialize the test runner."""
    super().__init__(*args, **kwargs)
    self.container_manager: ContainerManager | None = None
    self.original_settings: dict[str, Any] = {}

setup_test_environment(**kwargs)

Set up test environment and start containers.

Source code in src/django_testcontainers_plus/runner.py
def setup_test_environment(self, **kwargs: Any) -> None:
    """Set up test environment and start containers."""
    # Capture original settings before Django's setup_test_environment
    # overwrites them (e.g. EMAIL_BACKEND is set to locmem)
    context = {
        "original_email_backend": getattr(settings, "EMAIL_BACKEND", None),
    }

    super().setup_test_environment(**kwargs)

    self.container_manager = ContainerManager(settings, context=context)

    settings_updates = self.container_manager.start_containers()

    if settings_updates:
        apply_settings_updates(settings_updates, self.original_settings)
        recreate_database_connections()

    if self.verbosity >= 1:
        for provider_name in self.container_manager.active_containers.keys():
            print(f"Started {provider_name} container for testing")

teardown_test_environment(**kwargs)

Tear down test environment and stop containers.

Source code in src/django_testcontainers_plus/runner.py
def teardown_test_environment(self, **kwargs: Any) -> None:
    """Tear down test environment and stop containers."""
    restore_settings(self.original_settings)

    if self.container_manager:
        if self.verbosity >= 1:
            print("Stopping test containers...")
        self.container_manager.stop_containers()

    super().teardown_test_environment(**kwargs)

django_testcontainers_plus.exceptions.MissingDependencyError

Bases: DjangoTestcontainersError

Source code in src/django_testcontainers_plus/exceptions.py
class MissingDependencyError(DjangoTestcontainersError):
    def __init__(
        self,
        provider_name: str,
        extra_name: str,
        detected_in: str | None = None,
        original_error: Exception | None = None,
    ):
        """Initialize the error with helpful context.

        Args:
            provider_name: Name of the provider (e.g., "MySQL", "Redis")
            extra_name: Name of the pip extra to install (e.g., "mysql", "redis")
            detected_in: Where the need was detected (e.g., "DATABASES['default']")
            original_error: The original ImportError that triggered this
        """
        self.provider_name = provider_name
        self.extra_name = extra_name
        self.detected_in = detected_in
        self.original_error = original_error

        message = self._build_message()
        super().__init__(message)

    def _build_message(self) -> str:
        """Build a helpful error message with installation instructions."""
        lines = [
            f"\n{'=' * 70}",
            f"{self.provider_name} Support Not Installed",
            "=" * 70,
        ]

        if self.detected_in:
            lines.extend(
                [
                    f"\n{self.provider_name} was detected in your Django settings:",
                    f"  → {self.detected_in}",
                ]
            )
        else:
            lines.append(f"\n{self.provider_name} is configured but dependencies are missing.")

        lines.extend(
            [
                f"\nTo enable {self.provider_name} support, install the required dependencies:",
                f"  pip install django-testcontainers-plus[{self.extra_name}]",
                "\nOr install all providers:",
                "  pip install django-testcontainers-plus[all]",
            ]
        )

        if self.original_error:
            error_type = type(self.original_error).__name__
            lines.extend(
                [
                    f"\nOriginal error: {error_type}: {self.original_error}",
                ]
            )

        lines.extend(
            [
                "\nFor more information, see:",
                "  https://github.com/woodywoodster/django-testcontainers-plus#installation",
                "=" * 70,
            ]
        )

        return "\n".join(lines)

__init__(provider_name, extra_name, detected_in=None, original_error=None)

Initialize the error with helpful context.

Parameters:

Name Type Description Default
provider_name str

Name of the provider (e.g., "MySQL", "Redis")

required
extra_name str

Name of the pip extra to install (e.g., "mysql", "redis")

required
detected_in str | None

Where the need was detected (e.g., "DATABASES['default']")

None
original_error Exception | None

The original ImportError that triggered this

None
Source code in src/django_testcontainers_plus/exceptions.py
def __init__(
    self,
    provider_name: str,
    extra_name: str,
    detected_in: str | None = None,
    original_error: Exception | None = None,
):
    """Initialize the error with helpful context.

    Args:
        provider_name: Name of the provider (e.g., "MySQL", "Redis")
        extra_name: Name of the pip extra to install (e.g., "mysql", "redis")
        detected_in: Where the need was detected (e.g., "DATABASES['default']")
        original_error: The original ImportError that triggered this
    """
    self.provider_name = provider_name
    self.extra_name = extra_name
    self.detected_in = detected_in
    self.original_error = original_error

    message = self._build_message()
    super().__init__(message)

django_testcontainers_plus.exceptions.DjangoTestcontainersError

Bases: Exception

Source code in src/django_testcontainers_plus/exceptions.py
class DjangoTestcontainersError(Exception): ...