Testing Guide¶
All changes to azure-functions-scaffold-python must include tests. We aim for high reliability and 90% or greater test coverage.
Running Tests¶
To execute the full test suite:
To run tests with a detailed coverage report:
To run a specific test file or function:
Test Structure¶
| File | Lines | Description |
|---|---|---|
test_cli.py |
~558 | CLI command tests using Typer CliRunner |
test_scaffolder.py |
~437 | Project generation logic with the tmp_path fixture |
test_generator.py |
~171 | Logic for adding new functions to existing projects |
Test Patterns¶
CLI Tests¶
We use typer.testing.CliRunner to test the command-line interface.
- Success paths: Verify the command exits with code 0 and provides the expected output message.
- Error paths: Verify the command exits with code 1 or 2 for invalid arguments or execution errors.
- Interactive mode: Use the
inputparameter inrunner.invoketo simulate user responses for CLI prompts.
from typer.testing import CliRunner
from azure_functions_scaffold.cli import app
runner = CliRunner()
def test_new_command_creates_project(tmp_path):
result = runner.invoke(app, ["new", "my-api", "--destination", str(tmp_path)])
assert result.exit_code == 0
Scaffolder Tests¶
These tests focus on the core project creation logic. Use the tmp_path fixture to create a temporary directory for each test run.
- Trigger types: Verify that all supported triggers (HTTP, Timer, Queue, etc.) generate correct file structures.
- Naming validation: Ensure the scaffolder rejects invalid project names or function names.
- Presets: Test the resolution of project presets to ensure they include the correct dependencies.
Generator Tests¶
These tests ensure new functions can be added to existing projects without breaking them.
- Function addition: Verify that adding a second or third function correctly updates the project's file structure.
- Validation: Test that adding a duplicate function name fails gracefully.
- Error handling: Test the behavior when the target project directory is missing or invalid.
Coverage Configuration¶
The project is configured to track:
- All source files in src/azure_functions_scaffold/.
- Branch coverage for conditional logic.
- Exclusions for boilerplate code that does not require testing.
A minimum coverage of 90% is required. PRs that drop the coverage below this threshold will fail CI checks.
Writing New Tests¶
- Place tests in the
tests/directory. - Follow the
test_<module>.pynaming convention. - Include both success and error paths (edge cases).
- Use descriptive function names like
test_scaffolder_fails_on_duplicate_name.
CI Matrix¶
Each pull request is tested against a matrix of environments to ensure broad compatibility:
- OS:
ubuntu-latest - Python versions: 3.10, 3.11, 3.12, 3.13, 3.14 (Preview - allowed to fail)
Generated-project smoke tests¶
The templates-smoke.yml workflow scaffolds every template on every push that touches src/azure_functions_scaffold/templates/** or related code, then runs compileall, ruff, mypy, and pytest against each generated project. This catches template-level breakage that unit tests cannot - for example, a marker-string drift between generator.py constants and template comments.
Local equivalent:
tmpdir=$(mktemp -d)
cd "$tmpdir"
afs new smoke --python-version 3.12
cd smoke
python -m compileall -q .
ruff check . && mypy . && pytest -q
Azure End-to-End Tests¶
The .github/workflows/e2e-azure.yml workflow generates a scaffolded project, deploys it to Azure, and validates HTTP endpoints against a real Function App.
Workflow¶
- File:
.github/workflows/e2e-azure.yml - Trigger: Manual only (
workflow_dispatch) - Infrastructure: Azure Consumption plan,
koreacentralregion (AZURE_LOCATIONvariable) - Cleanup: Resource group deleted immediately after tests (
if: always())
Required Secrets & Variables¶
The Azure OIDC secrets (AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID) are provided as repository-level secrets, matching the sibling azure-functions-* repositories. AZURE_LOCATION is read from the vars context with a fallback to koreacentral.
| Name | Type | Description |
|---|---|---|
AZURE_CLIENT_ID |
Secret | App Registration Client ID (OIDC) |
AZURE_TENANT_ID |
Secret | Azure Tenant ID |
AZURE_SUBSCRIPTION_ID |
Secret | Azure Subscription ID |
AZURE_LOCATION |
Variable | Azure region (default: koreacentral) |
Federated Credential (OIDC) Setup¶
Azure login uses GitHub OIDC to exchange a short-lived token with the Azure AD app registration referenced by AZURE_CLIENT_ID. The app registration must have a federated credential whose subject claim matches exactly the value GitHub presents.
For this workflow, the expected subject is:
The subject is composed of repo:<owner>/<repo>:ref:<git_ref>, where:
<owner>/<repo>is the GitHub repository slug (github.repository). Not the PyPI package name (azure-functions-scaffold-python) or the Python import name (azure_functions_scaffold).<git_ref>is the ref the workflow runs against. Because the workflow is dispatch-only and dispatched frommain, GitHub mintsref:refs/heads/main.
The match is case-sensitive and exact. Renaming the GitHub owner or repository, or dispatching from a different branch, requires updating the federated credential in Azure to match the new subject; otherwise Azure login fails with AADSTS700213.
Why a ref-based subject (and no GitHub environment)?¶
This workflow does not declare a GitHub environment:, matching the sibling azure-functions-* repositories (openapi, logging, doctor). Since the workflow is dispatch-only and always dispatched from main, GitHub mints a single stable ref-based subject (ref:refs/heads/main), so exactly one federated credential is required. A GitHub environment would instead mint environment:<name> and require a matching environment-scoped credential.
If you later want approval gating (required reviewers or wait timers) before the destructive Azure run, declare
environment: azure-e2eon both jobs and add an environment-scoped federated credential (...:environment:azure-e2e) to the app registration. Do both together, or Azure login will fail withAADSTS700213.
Reference:
- Azure docs: Configure a federated identity credential on an app.
Troubleshooting Azure E2E¶
AADSTS700213: No matching federated identity record found¶
The OIDC subject GitHub presented does not match any federated credential on the Azure AD app registration behind AZURE_CLIENT_ID. Typical causes:
- The repository was renamed (for example, the toolkit-wide
-pythonsuffix migration) and the federated credential still references the old subject. - A GitHub
environment:was added to the workflow job, so GitHub now mints anenvironment:<name>subject, but the credential still references the ref-based subject (or vice versa). - A different app registration is configured in the repository secrets than the one carrying the federated credential.
- The workflow was dispatched from a branch other than
main, mintingref:refs/heads/<branch>with no matching credential.
To recover:
- Confirm which Azure AD app registration is referenced by the repository's
AZURE_CLIENT_IDvalue. - On that app registration, add or update a federated credential with subject
repo:yeongseon/azure-functions-scaffold-python:ref:refs/heads/main, issuerhttps://token.actions.githubusercontent.com, and audienceapi://AzureADTokenExchange. - Re-run
e2e-azureviaworkflow_dispatchand confirm theAzure login (OIDC)step succeeds.
Troubleshooting¶
- Temporary files: If a test fails,
tmp_pathis automatically cleaned up. To inspect generated files, you can print the path during a local run. - Template rendering: Use
pytest -sto see output if you suspect a Jinja2 rendering error. - Imports: Ensure you are running tests via
make testorhatch run pytestto correctly set the Python path for thesrcdirectory.