Project Style Guide¶
This document outlines the coding style and conventions for the azure-functions-test project. All contributors and tools (including GitHub Copilot) must adhere to these standards.
General Coding Standards¶
PEP 8 Compliance¶
We use Ruff to enforce PEP compliance throughout the codebase and expect all code to adhere to the following standards:
- Follow PEP 8 guidelines for Python code.
Line Width¶
- Maximum line width: 88 characters.
- Use consistent line breaks to maintain readability.
Indentation¶
- Use 4 spaces per indentation level.
- Do not use tabs.
Spelling Convention¶
- Use American English spellings throughout the codebase (to match Azure Functions SDK conventions).
- Examples of preferred spellings:
behaviornotbehaviourinitializenotinitialisesynchronizenotsynchroniseserializenotserialise- Exception: When referencing Azure Functions SDK classes/methods, use their exact spelling (e.g.,
QueueMessage,HttpRequest)
Docstring Style¶
General Docstring Guidelines¶
- All public functions, classes, and modules must have docstrings.
- Use Google-style docstrings for documentation.
- Private functions (prefixed with
_) should have docstrings if their logic is non-trivial.
Google-Style Docstring Example¶
def mock_queue_message(
body: dict | list | str | bytes | None = None,
*,
id: str | None = None,
dequeue_count: int | None = None,
) -> QueueMessageMock:
"""Create a mock QueueMessage for testing.
Args:
body: Message body. Dicts/lists are JSON-serialized automatically.
id: Message ID. Defaults to "test-message-id".
dequeue_count: Number of times dequeued. Defaults to 1.
Returns:
A mock that behaves like azure.functions.QueueMessage.
Example:
>>> msg = mock_queue_message({"order_id": 123})
>>> msg.get_json()
{'order_id': 123}
"""
pass
Key Points: - There's no need to include type hints again in the docstring, as they are already present in the function signature. - The function signature is authoritative for types. - Include examples for public API functions to show typical usage.
Type Hints¶
Use Primitive Types Where Available¶
- Use primitive types (
list,dict,tuple,set) instead of theirtypingmodule equivalents. - Prefer
list[str]overList[str](Python 3.9+) - Prefer
dict[str, int]overDict[str, int](Python 3.9+) - Prefer
tuple[str, int]overTuple[str, int](Python 3.9+) - Use
T | Noneinstead ofOptional[T](Python 3.10+) - Use
str | intinstead ofUnion[str, int](Python 3.10+)
Good:
def process_messages(items: list[str]) -> dict[str, int]:
"""Process list of items and return counts."""
return {item: len(item) for item in items}
def get_config(name: str) -> dict[str, str] | None:
"""Get config by name, returns None if not found."""
return None
Avoid:
from typing import Dict, List, Optional
def process_messages(items: List[str]) -> Dict[str, int]:
"""Process list of items and return counts."""
return {item: len(item) for item in items}
def get_config(name: str) -> Optional[Dict[str, str]]:
"""Get config by name, returns None if not found."""
return None
Exception: Use typing for Complex Types¶
Continue using typing module for complex type constructs:
typing.Any(no primitive equivalent)typing.TypeVar(for generics)typing.Generic(for generic classes)typing.Protocol(for structural subtyping)typing.Callable[[str], bool](function types)
Minimum Python Version: 3.9¶
Since we target Python 3.9+:
- Use list[T], dict[K, V], tuple[T, ...], set[T]
- For Python 3.10+ features (| union syntax), use from __future__ import annotations at the top of the file
Type Safety and Data Structures¶
Use Dataclasses or Pydantic for Structured Data¶
Rule: Always use typed dataclasses instead of raw dictionaries for application data.
Working with raw dictionaries (dict[str, Any]) in application logic is not type-safe and should be avoided. Use strongly-typed dataclasses to ensure:
- Type checking catches errors at development time
- IDE autocomplete and refactoring support
- Clear contracts between components
- Self-documenting code
Good:
from dataclasses import dataclass
@dataclass
class MockConfig:
"""Configuration for mock object behaviour."""
id: str
dequeue_count: int
auto_serialize: bool = True
def create_mock(config: MockConfig) -> QueueMessageMock:
"""Create mock with type-safe configuration."""
return QueueMessageMock(id=config.id, dequeue_count=config.dequeue_count)
Avoid:
def create_mock(config: dict[str, Any]) -> QueueMessageMock:
"""Create mock without type safety."""
return QueueMessageMock(id=config["id"], dequeue_count=config["dequeue_count"])
Exception: Azure Functions SDK Compatibility¶
When interfacing with the Azure Functions SDK, match their API contracts exactly:
- If the SDK expects dict, use dict
- If the SDK returns dict, immediately convert to typed models for internal use
from dataclasses import dataclass
import typing as t
@dataclass
class HttpRequestData:
"""Type-safe representation of HTTP request data."""
method: str
url: str
headers: dict[str, str]
@classmethod
def from_sdk_request(cls, request: HttpRequest) -> "HttpRequestData":
"""Convert from SDK type at boundary."""
return cls(
method=request.method,
url=request.url,
headers=dict(request.headers),
)
Test Naming Conventions¶
- Test function names must be descriptive and use lowercase with underscores.
- Test names should clearly state the scenario and expected outcome.
- Use the following pattern for test names:
Examples:
test_queue_message_with_dict_body_serializes_to_jsontest_context_output_not_set_raises_value_error-
test_http_mock_with_custom_headers_returns_correct_values -
Avoid generic names like
test_somethingortest_case1. - The test name should make it clear what is being tested and what the expected behavior is.
Module Organization¶
Domain-Driven Module Structure¶
All modules should follow a consistent organization that prioritizes the "need-to-know" principle. Sections are ordered from most important (public API) to least important (implementation details).
Recommended Section Order¶
"""Module docstring describing domain responsibility."""
from __future__ import annotations
# =============================================================================
# IMPORTS
# =============================================================================
# Standard library
import json
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timezone
# Third-party
from azure.functions import QueueMessage
# Project/local
from .base import BaseMock
# =============================================================================
# TYPES & CONSTANTS
# =============================================================================
DEFAULT_MESSAGE_ID = "test-message-id"
DEFAULT_DEQUEUE_COUNT = 1
# =============================================================================
# PUBLIC API
# =============================================================================
def mock_queue_message(
body: dict | list | str | bytes | None = None,
*,
id: str | None = None,
) -> QueueMessageMock:
"""Create a mock QueueMessage for testing."""
pass
# =============================================================================
# CORE CLASSES
# =============================================================================
class QueueMessageMock(BaseMock[QueueMessage]):
"""Mock implementation of azure.functions.QueueMessage."""
pass
# =============================================================================
# PRIVATE HELPERS
# =============================================================================
def _serialize_body(body: dict | list | str | bytes) -> bytes:
"""Convert various body types to bytes."""
pass
Rationale¶
- Need-to-know: Readers see the module's interface immediately
- API-driven: Public functions define what the module does
- Top-down reading: Start with high-level concepts, drill down to implementation
- Maintainability: Easy to identify what can be refactored vs what's part of the public contract
- Consistency: Same pattern across all domain modules
SOLID Principles in Practice¶
This library is explicitly designed around SOLID principles:
Single Responsibility Principle (SRP)¶
- Each mock class handles one trigger type (Queue, HTTP, Timer, etc.)
FunctionTestContexthandles only output capture, not input mocking- Factory functions handle only object creation, not behavior
Open/Closed Principle (OCP)¶
BaseMockprovides extension points for new mock types- New trigger mocks extend
BaseMockwithout modifying existing code - Use abstract methods to enforce contracts
Liskov Substitution Principle (LSP)¶
- All mocks are drop-in replacements for real SDK types
- Duck-type compatibility: if
QueueMessagehas a method, our mock has it too - Clients should not distinguish between real and mock objects
Interface Segregation Principle (ISP)¶
- Small, focused interfaces (e.g.,
BaseMock._build(),BaseMock._default_values()) - Clients depend only on methods they use
- No "fat interfaces" with unused methods
Dependency Inversion Principle (DIP)¶
- Depend on abstractions (
BaseMock) not concrete implementations - High-level modules (tests) don't depend on low-level modules (mock implementations)
Error Handling¶
Fail Fast, Fail Clear¶
- Validate inputs at function entry points
- Raise descriptive exceptions with actionable error messages
- Use built-in exception types when appropriate (
ValueError,TypeError,KeyError)
Good:
def mock_queue_message(body: dict | None = None) -> QueueMessageMock:
"""Create a mock QueueMessage."""
if body is not None and not isinstance(body, (dict, list, str, bytes)):
raise TypeError(
f"body must be dict, list, str, or bytes, got {type(body).__name__}"
)
return QueueMessageMock(body=body)
Avoid:
def mock_queue_message(body: dict | None = None) -> QueueMessageMock:
"""Create a mock QueueMessage."""
# Silently coerces or fails later with obscure error
return QueueMessageMock(body=body)
Performance Guidelines¶
Optimization Principles¶
- Measure First: Always profile before optimizing
- Lazy Evaluation: Build mock objects only when accessed (see
BaseMock.build()) - Reuse Instances: Cache built mock instances to avoid redundant construction
- Simple is Fast: Prefer straightforward code; complexity rarely pays off in test utilities
Common Patterns¶
class BaseMock:
"""Base mock with lazy build pattern."""
def __init__(self, **overrides: Any) -> None:
"""Initialize with config, but don't build yet."""
self._values = {**self._default_values(), **overrides}
self._instance: T | None = None
def build(self) -> T:
"""Build the mock instance lazily."""
if self._instance is None:
self._instance = self._build()
return self._instance
Git Commit Convention¶
- Use clear, descriptive commit messages
- Follow the format:
<type>: <description> - Types:
feat,fix,docs,style,refactor,test,chore
Examples:
feat: add ServiceBus mock supportfix: handle None body in queue messagesrefactor: extract common validation logic to BaseMockdocs: add examples for HTTP mock usagetest: add edge case coverage for context output capture
Questions?¶
If you're unsure about any style conventions, check this guide first. When in doubt: 1. Follow PEP 8 2. Prioritize type safety 3. Keep it simple (YAGNI - You Aren't Gonna Need It) 4. Match Azure Functions SDK conventions for compatibility