Skip to content
Kelly Ferrone edited this page Mar 7, 2026 · 2 revisions

Running Tests

Prerequisites

export DUPLO_HOST=https://your-portal.duplocloud.net
export DUPLO_TOKEN=<your-token>

Install with test dependencies:

pip install --editable '.[build,test,aws]'

Quick Reference

# Unit tests only — fast, no credentials needed
pytest src -m unit

# All integration tests — expensive, slow, creates real cloud resources
pytest src -m integration

# Integration tests for a specific cloud
pytest src -m "integration and aws"

# Integration tests for a specific infra type
pytest src -m "integration and k8s"
pytest src -m "integration and ecs"

# Combine cloud + infra type
pytest src -m "integration and aws and k8s"

# Run only against an existing infra and tenant (skips create/destroy)
pytest src -m integration --infra testk8siso --tenant mytenantname

CLI Arguments

These are passed directly to pytest and are defined in conftest.py.

Argument Env var fallback Description
--infra <name> DUPLO_INFRA Infrastructure name. Create is skipped if it already exists.
--tenant <name> DUPLO_TENANT Tenant name. Create is skipped if it already exists.

Tip: When iterating on a single resource, always pass --infra and --tenant pointing to something already live so the expensive create/destroy steps are skipped.


Name Resolution

infra_name and tenant_name are session fixtures that determine what infrastructure and tenant the test run targets. Their values are resolved at fixture setup time using the following rules.

infra_name resolution

What you pass What happens
--infra foo infra = foo (always wins)
--tenant bar (no --infra), tenant bar exists infra = bar's PlanID (looked up from the portal)
--tenant bar (no --infra), tenant bar does not exist infra = bar (tenant and infra share the same name)
DUPLO_TENANT=bar (no CLI args), same lookup as above same rules as --tenant bar
nothing given infra = duploctl{1000–9999} (random, unique per run)

tenant_name resolution

Once infra_name is known, tenant_name resolves as:

Priority Source
1 --tenant <name> explicit CLI arg
2 DUPLO_TENANT environment variable
3 infra_name (DuploCloud convention: tenant = infra name)

Safety guard

pytest_configure runs before any fixtures and validates consistency when both --infra and a tenant name are known and differ.

If the resolved tenant already exists on a different infra, pytest exits immediately:

ERROR: Tenant 'mytenant' already exists but belongs to infra 'otheinfra', not 'myinfra'.
Either pass --infra otherinfra to match the tenant's infra,
or choose a --tenant name that does not exist yet.

Common scenarios

# Existing infra, same-named tenant (the default convention)
pytest src -m integration --infra testk8siso
# infra_name = "testk8siso", tenant_name = "testk8siso"

# Existing infra, different existing tenant
pytest src -m integration --infra testk8siso --tenant myteam
# infra_name = "testk8siso", tenant_name = "myteam"
# (guard checks that tenant 'myteam' belongs to 'testk8siso')

# Only tenant given — infra is inferred from the tenant's PlanID
pytest src -m integration --tenant myteam
# Looks up 'myteam'; if its PlanID is "testk8siso" → infra_name = "testk8siso"

# Only tenant given — tenant doesn't exist yet (will be created)
pytest src -m integration --tenant newstuff
# infra_name = "newstuff", tenant_name = "newstuff"
# (both will be created from scratch)

# Via env var — same rules as --tenant
export DUPLO_TENANT=myteam
pytest src -m integration

# Fully automatic — random infra+tenant name, full lifecycle
pytest src -m integration

Markers

Markers are combined with -m using boolean expressions (and, or, not).

Test type

Marker When to use
unit Pure logic tests. No network, no credentials. Always fast.
integration Tests that hit a live DuploCloud portal and create/modify real resources.

Cloud provider

Marker Description
aws Tests that are AWS-specific
gcp Tests that are GCP-specific
azure Tests that are Azure-specific

Infrastructure type

These mark tests that require a specific kind of infrastructure to be running.

Marker Infrastructure Notes
k8s EKS / GKE / AKS Tests that require a Kubernetes cluster
ecs ECS (AWS only) Tests for ECS services, task definitions, etc.
duplo Duplo-native No k8s, no ECS — simplest infra type

Dependencies & Ordering

Integration tests use two pytest plugins together:

  • pytest-order — enforces global execution order via @pytest.mark.order(N)
  • pytest-dependency — skips a test automatically if any named dependency did not pass

Ordering scheme

Order numbers follow a range-based scheme so each resource group has room to grow without colliding with others. Lower numbers are reserved for foundational lifecycle steps; higher numbers are for per-resource tests; the 990s are for teardown.

Range What runs
1 create infra
2 list + find infra (read-only validation)
10 create tenant
11 list + find tenant
12 additional tenant reads (users, billing, region, DNS)
20–24 hosts: create → find → stop → start → reboot
30–34 ASG: create → find → update → list → scale
40–41 bulk tenant resources: create → find (parametrized)
50–52 configmap: create → find → update
60–62 secret: create → find → update
70–72 ingress: create → update → list
75–76 job: create → find/list/pods
80–81 cronjob mutations
90–94 cloudfront: create → update → list → disable → enable
100–102 AWS secret: create → find → update
110 JIT credential tests
120–122 AI helpdesk: create ticket → variations → send message
993–997 per-resource deletes
998 delete tenant
999 delete infra

Create-then-find pairs

Every resource follows the pattern: create immediately precedes find. The find step registers a named dependency (name="find_<resource>") that later tests depend on. This ensures nothing operates on a resource that wasn't confirmed to exist.

Compute-first ordering

Hosts (20–24) and ASGs (30–34) run before bulk tenant resources (40+) because they are infrastructure prerequisites — the Kubernetes nodes need to exist before workloads can be scheduled.

Jobs (75–76) carry depends=["find_asg"] explicitly because a running node is required for a job to complete.

Idempotent create / ownership guards

All create_* tests check if the resource already exists before calling the API. If it does, the test returns early (passing) rather than failing. This makes it safe to re-run a test suite against a live environment.

Two fixtures control teardown safety:

  • owns_infraFalse if --infra was passed and the infra pre-existed, or if the infra name was inferred from an existing tenant's PlanID.
  • owns_tenantFalse if owns_infra is False, or if the explicit tenant already existed before the run.

The test_find_delete_infra and test_find_delete_tenant tests call pytest.skip() when ownership is False, so pre-existing infrastructure is never destroyed.

Dependency registration

A dependency name (e.g. "find_tenant_resource") is only registered when the test that declares name= is collected. Always run with a marker that includes the lifecycle tests (TestInfra, TestTenant, etc.) — they carry the same cloud/infra markers as the resources they gate. Running only -m k8s without also collecting these lifecycle classes will cause all downstream deps to warn and skip.


Example Workflows

Isolated infra smoke test (create + destroy only)

pytest src/tests/test_infra.py -m integration -v

K8s resources only against an existing infra

pytest src -m "integration and k8s" \
  --infra testk8siso \
  --tenant mytenant \
  -v

All AWS integration tests (the full expensive run)

pytest src -m "integration and aws" -v

Just unit tests in CI

pytest src -m unit

Infra Types

Type Orchestrator Key resources
k8s EKS / GKE / AKS Services, configmaps, secrets, ingress, cronjobs, jobs
ecs ECS (AWS only) ECS services, task definitions
duplo None Hosts, RDS, S3, Lambda, ASG only

Always pass --infra / --tenant to target an existing environment and avoid provisioning a new one unnecessarily.

Notes on Cleanup

Failed or leftover infras accumulate over time and consume AWS quotas even in a broken state. After any test run that creates an infra, verify cleanup happened:

duploctl infrastructure list --query "[].{Name:Name,Status:ProvisioningStatus}"

Any infra stuck in *Failed status should be deleted manually:

duploctl infrastructure delete <name>

Clone this wiki locally