Testing
This project uses pytest for all automated testing. Tests are split into unit tests (fast, fully mocked, no cloud credentials) and integration tests (hit real cloud APIs, require credentials and environment variables).
Quick Start
# Install the package with test dependencies
pip install -e ".[test]"
# Run all unit tests (the default — no credentials needed)
pytest
# Run with a coverage report
pytest --cov=auto_proxy_vpn --cov-report=html
open htmlcov/index.html # macOS
# Report to terminal
pytest --cov=auto_proxy_vpn --cov-report=term-missing
Test Dependencies
All testing dependencies are declared in pyproject.toml under [project.optional-dependencies] test:
Package |
Purpose |
|---|---|
|
Test runner and assertion framework |
|
Coverage measurement and reporting |
|
|
|
Mock |
Test Structure
tests/
├── conftest.py # Shared fixtures, stubs, helpers
├── README.md # Quick-start guide for contributors
├── unit/ # Mocked tests — fast, deterministic
│ ├── test_configs.py # Config dataclasses & validation
│ ├── test_manager_register.py # ProxyManagers registry
│ ├── test_base_proxy.py # BaseProxy, ProxyBatch, BaseProxyManager
│ ├── test_proxy_pool.py # ProxyPool, RandomManagerPicker, regions, running names
│ ├── test_utils.py # Public IP detection, SSH, exceptions
│ ├── test_digitalocean_proxy.py # DigitalOcean provider (mocked HTTP)
│ ├── test_digitalocean_utils.py # DigitalOcean utility functions
│ ├── test_google_proxy.py # Google Cloud provider (mocked SDK)
│ ├── test_azure_proxy.py # Azure provider (mocked SDK)
│ ├── test_aws_proxy.py # AWS provider (mocked SDK)
│ └── test_aws_utils.py # AWS utility functions
└── integration/ # Real cloud tests — slow, needs creds
├── test_proxy_pool_real.py # ProxyPool end-to-end with available real provider creds
├── test_digitalocean_real.py
├── test_google_real.py
├── test_azure_real.py
└── test_aws_real.py
Markers
Pytest markers control which tests run. They are defined in pyproject.toml:
Marker |
Description |
|---|---|
|
Unit tests with mocked dependencies |
|
Tests that hit real cloud APIs |
|
Requires a DigitalOcean API token |
|
Requires Google Cloud credentials |
|
Requires Azure credentials |
|
Requires AWS credentials |
Filtering by marker
# Only unit tests (this is the default)
pytest
# Only integration tests
pytest -m integration
# DigitalOcean integration tests only
pytest -m "integration and digitalocean"
Unit Tests
Unit tests are the backbone of the test suite. They run in milliseconds, require no cloud credentials, and cover:
Core — config dataclasses, the
ProxyManagersregistry,ProxyPool,RandomManagerPicker.ProxyPool details — account-aware
get_running_proxy_names(), provider region helpers, and batch distribution.Base classes —
BaseProxy,ProxyBatch,BaseProxyManager(usingStubProxy/StubProxyManager).Providers — each provider is tested with its external SDK or HTTP API fully mocked.
Mocking Strategy
Each provider has a different external dependency surface, so the mocking approach varies:
DigitalOcean
DigitalOcean calls the REST API with the requests library. We use the responses library to intercept and mock HTTP calls:
import responses
@responses.activate
def test_something(self, digitalocean_config):
responses.add(responses.GET, f"{DO_API}/regions", json={...}, status=200)
# ... rest of the test
Google Cloud
Google Cloud uses the google-cloud-compute SDK which is not installed in CI. We mock the entire module hierarchy via sys.modules:
from unittest.mock import patch, MagicMock
from types import SimpleNamespace
# 1. Build mock objects
compute_v1 = MagicMock()
images_client = MagicMock()
images_client.list.return_value = SimpleNamespace(
items=[SimpleNamespace(name="ubuntu-minimal-2404-noble-amd64-v20250101")]
)
# 2. Wire parent → child relationships
# (from google.cloud import compute_v1 resolves via
# getattr(sys.modules['google.cloud'], 'compute_v1'))
google_cloud_mock = MagicMock()
google_cloud_mock.compute_v1 = compute_v1
# 3. Patch sys.modules
with patch.dict("sys.modules", {
"google": google_mock,
"google.cloud": google_cloud_mock,
"google.cloud.compute_v1": compute_v1,
# ... etc.
}):
from auto_proxy_vpn.providers.google.google_proxy import ProxyManagerGoogle
Important
When mocking sys.modules, the parent module mock must expose
the child mock as an attribute. Otherwise from parent import child
silently gets an auto-generated MagicMock instead of your configured
one.
Azure
Azure uses multiple azure-mgmt-* SDKs. The approach is the same as Google — mock the module hierarchy via sys.modules and wire parent → child attributes.
AWS
AWS unit tests mock the SDK surface used by the provider (boto3 and botocore.exceptions) instead of making real EC2 calls. The common pattern is:
Build a fake SDK with
MagicMock(client(),resource(),describe_regions(),create_instances(), etc.).Patch imports with
patch.dict("sys.modules", ...)soProxyManagerAwsloads the mocked modules.Validate behavior by asserting calls and side effects (for example, security-group cleanup on errors).
from unittest.mock import MagicMock, patch
boto3_mock = MagicMock()
client = MagicMock()
resource = MagicMock()
client.describe_regions.return_value = {
"Regions": [{"RegionName": "us-east-1"}]
}
boto3_mock.client.return_value = client
boto3_mock.resource.return_value = resource
class MockClientError(Exception):
def __init__(self, code: str):
self.response = {"Error": {"Code": code}}
with patch.dict("sys.modules", {
"boto3": boto3_mock,
"botocore": MagicMock(),
"botocore.exceptions": MagicMock(ClientError=MockClientError),
}):
from auto_proxy_vpn.providers.aws.aws_proxy import ProxyManagerAws
Common Pitfalls
Pitfall |
Solution |
|---|---|
|
Pass |
|
Use |
|
Wire |
|
Implement |
Integration Tests
Integration tests create real cloud resources and are skipped unless the required environment variables are set.
All integration tests call load_dotenv(...) and load variables from a .env file
at the repository root when present.
Warning
Integration tests create real cloud resources that cost money. They
include cleanup logic in finally blocks, but always verify resources
are destroyed after a run.
Required Environment Variables
DigitalOcean
export DIGITALOCEAN_API_TOKEN="dop_v1_..."
export SSH_KEY="ssh-rsa AAAA..."
pytest -m integration -k digitalocean
Google Cloud
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json"
export GOOGLE_PROJECT="my-project-id"
export SSH_KEY="ssh-rsa AAAA..."
pytest -m integration -k google
Azure
export AZURE_SUBSCRIPTION_ID="..."
export AZURE_TENANT_ID="..."
export AZURE_CLIENT_ID="..."
export AZURE_CLIENT_SECRET="..."
export SSH_KEY="ssh-rsa AAAA..."
pytest -m integration -k azure
AWS
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export SSH_KEY="ssh-rsa AAAA..."
pytest -m integration -k aws
Writing Tests
Conventions
Group related tests in classes:
class TestProxyManagerInit:Name tests descriptively:
test_<what>_<condition>_<expected>Use
conftest.pyfixtures for shared setupMock external calls — never let a unit test touch the network
Integration tests must clean up cloud resources in
finallyblocks
Adding Tests for a New Provider
Create
tests/unit/test_<provider>_proxy.pyfor mocked tests.Create
tests/integration/test_<provider>_real.pyfor real-cloud tests.Add a marker in
pyproject.tomlunder[tool.pytest.ini_options].Add the integration CI job in
.github/workflows/tests.yml.Add required secrets to the table below.
Test Naming
class TestProxyManagerFooGetProxy:
def test_get_proxy_creates_instance(self): # happy path
...
def test_get_proxy_invalid_region_raises(self): # error case
...
def test_get_proxy_duplicate_name_raises(self): # edge case
...
GitHub Secrets
To enable integration tests in CI, configure these repository secrets under Settings → Secrets and variables → Actions:
Secret |
Provider |
|---|---|
|
DigitalOcean |
|
DigitalOcean, Google Cloud, Azure, AWS |
|
Google Cloud |
|
Google Cloud |
|
Azure |
|
Azure |
|
Azure |
|
Azure |
|
AWS |
|
AWS |
Coverage
Coverage is measured with pytest-cov and reported in CI as an XML artifact.
# Terminal report
pytest --cov=auto_proxy_vpn --cov-report=term-missing
# HTML report (opens in browser)
pytest --cov=auto_proxy_vpn --cov-report=html
open htmlcov/index.html
The coverage configuration in pyproject.toml ensures only the auto_proxy_vpn/ source tree is measured — the tests/ directory itself is excluded.