DEV Community

Cover image for Python Typed Parameterized Decorators in Test Automation
Nikolai Filatov
Nikolai Filatov

Posted on

Python Typed Parameterized Decorators in Test Automation

Python's decorator functionality, enriched with the latest type hinting features, transforms how we approach test automation. By leveraging Python's flexibility with decorators and the enhanced type safety provided by the typing module, we can create more maintainable, readable, and robust test suites. This article delves into how to use these advanced features, focusing on their application within test automation frameworks.

Latest Features from the Typing Module

Python's typing module has seen significant updates:

  • PEP 585 introduces native support for generic types in standard collections, reducing the use of the typing module for common types
  • PEP 604 introduces the | operator for Union types, simplifying type annotations
  • PEP 647 introduces TypeAlias for clearer type alias definitions
  • PEP 649 allows for deferred evaluation of annotations, improving startup time for large projects

Creating Typed Parameterized Decorators

Here's how to craft a decorator using these new typing features:

from typing import Protocol, TypeVar, Generic, Callable, Any
from functools import wraps

# TypeVar for generic typing
T = TypeVar('T')

# Protocol for defining function structure
class TestProtocol(Protocol):
    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        ...

def generic_decorator(param: str) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """
    A generic decorator that can wrap any function returning type T.

    Args:
        param: A string parameter used by the decorator

    Returns:
        A callable that wraps the original function
    """
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)  # Preserves the metadata of the original function
        def wrapper(*args: Any, **kwargs: Any) -> T:
            print(f"Generic decorator with param: {param}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@generic_decorator("test_param")
def test_function(x: int) -> int:
    """A test function that returns the input multiplied by 2."""
    return x * 2
Enter fullscreen mode Exit fullscreen mode

This decorator uses Protocol to define what a test function should look like, enhancing flexibility when dealing with different function signatures in test frameworks.

Applying to Test Automation Frameworks

Let's explore how these decorators can be used in test automation:

1. Platform-Specific Tests with Literal

from typing import Literal, Callable, Any
import sys

def run_only_on(platform: Literal["linux", "darwin", "win32"]) -> Callable:
    """
    Decorator to ensure a test function runs only on specified platforms.

    Args:
        platform: The target platform for the test

    Returns:
        A callable that wraps the original test function
    """
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            if sys.platform == platform:
                return func(*args, **kwargs)
            print(f"Skipping test for platform: {sys.platform}")
            return None
        return wrapper
    return decorator

@run_only_on("linux")
def test_linux_feature() -> None:
    """Linux-specific test."""
    # Linux-specific operations
    pass
Enter fullscreen mode Exit fullscreen mode

The Literal type ensures type checkers know exactly which values are valid for the platform parameter, making it clear which tests run on which platforms. This is particularly useful in cross-platform testing frameworks.

2. Timeout Decorators with Threading

from typing import Callable, Any, Optional
import threading
import time
from concurrent.futures import ThreadPoolExecutor, TimeoutError

def timeout(seconds: int) -> Callable:
    """
    Decorator to enforce a timeout on test functions.

    Args:
        seconds: Maximum execution time in seconds

    Returns:
        A callable that wraps the original function with timeout functionality
    """
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Optional[Any]:
            with ThreadPoolExecutor(max_workers=1) as executor:
                future = executor.submit(func, *args, **kwargs)
                try:
                    return future.result(timeout=seconds)
                except TimeoutError:
                    print(f"Function {func.__name__} timed out after {seconds} seconds")
                    return None
        return wrapper
    return decorator

@timeout(5)
def test_long_running_operation() -> None:
    """A test that times out if it takes too long."""
    time.sleep(10)  # This will trigger a timeout
Enter fullscreen mode Exit fullscreen mode

This implementation uses Python's built-in threading capabilities to implement a reliable timeout mechanism, suitable for test automation scenarios where execution time must be constrained.

3. Retry Mechanism with Union Types

from typing import Callable, Any, Union, Type, Tuple, Optional
import time

def retry_on_exception(
    exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]], 
    attempts: int = 3,
    delay: float = 1.0
) -> Callable:
    """
    Decorator to retry a function on specified exceptions.

    Args:
        exceptions: Single exception or tuple of exceptions to catch
        attempts: Maximum number of retry attempts
        delay: Delay between attempts in seconds

    Returns:
        A callable that wraps the original function with retry logic
    """
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            last_exception: Optional[Exception] = None

            for attempt in range(attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    print(f"Attempt {attempt + 1} failed with {type(e).__name__}: {str(e)}")
                    if attempt < attempts - 1:
                        time.sleep(delay)

            if last_exception is not None:
                raise last_exception
            return None
        return wrapper
    return decorator

@retry_on_exception((TimeoutError, ConnectionError), attempts=5, delay=1.0)
def test_network_connection() -> None:
    """Test network connection with retry logic."""
    # Simulate network operations
    pass
Enter fullscreen mode Exit fullscreen mode

This improved version includes comprehensive type hints, robust exception handling, and a configurable delay between retries. It uses Union types to accept either a single exception type or a tuple of exception types, providing flexibility for various testing scenarios.

Conclusion

The integration of Python's latest typing features into decorators enhances both type safety and code readability while improving test automation framework capabilities. By defining explicit behavior through types, you ensure that tests execute under the right conditions with proper error handling and performance constraints. This approach leads to more robust, maintainable, and efficient testing processes, particularly beneficial in large-scale, distributed, or multi-platform test environments.

Neon image

Serverless Postgres in 300ms (❗️)

10 free databases with autoscaling, scale-to-zero, and read replicas. Start building without infrastructure headaches. No credit card needed.

Try for Free →

Top comments (0)

Sentry image

Make it make sense

Only the context you need to fix your broken code with Sentry.

Start debugging →

👋 Kindness is contagious

Dive into this informative piece, backed by our vibrant DEV Community

Whether you’re a novice or a pro, your perspective enriches our collective insight.

A simple “thank you” can lift someone’s spirits—share your gratitude in the comments!

On DEV, the power of shared knowledge paves a smoother path and tightens our community ties. Found value here? A quick thanks to the author makes a big impact.

Okay