|
| 1 | +# ACP Python SDK Automated Testing |
| 2 | + |
| 3 | +<details> |
| 4 | +<summary>📑 Table of Contents</summary> |
| 5 | + |
| 6 | +``` |
| 7 | +tests/ |
| 8 | +├── unit/ # Unit tests (mocked dependencies) |
| 9 | +│ ├── test_account.py |
| 10 | +│ ├── test_client.py |
| 11 | +│ ├── test_contract_client_v2.py |
| 12 | +│ ├── test_fare.py |
| 13 | +│ ├── test_job.py |
| 14 | +│ ├── test_job_offering.py |
| 15 | +│ ├── test_memo.py |
| 16 | +│ └── test_x402.py |
| 17 | +│ |
| 18 | +├── integration/ # Integration tests (real network calls) |
| 19 | +│ ├── test_client_integration.py |
| 20 | +│ └── test_integration_contract_client_v2.py |
| 21 | +│ |
| 22 | +├── conftest.py # Pytest fixtures and configuration |
| 23 | +├── .env.example # Environment variable template |
| 24 | +└── .env # Environment variables (gitignored) |
| 25 | +``` |
| 26 | + |
| 27 | +- [Introduction](#introduction) |
| 28 | + - [Purpose](#purpose) |
| 29 | +- [Running Tests](#running-tests) |
| 30 | + - [All Tests](#all-tests) |
| 31 | + - [Unit Tests Only](#unit-tests-only) |
| 32 | + - [Integration Tests Only](#integration-tests-only) |
| 33 | + - [Specific Test Files](#specific-test-files) |
| 34 | + - [Generating Coverage Report](#generate-coverage-report) |
| 35 | +- [How to Write Tests](#how-to-write-tests) |
| 36 | + - [Unit Tests](#unit-tests) |
| 37 | + - [Integration Tests](#integration-tests) |
| 38 | + |
| 39 | +</details> |
| 40 | + |
| 41 | +## Introduction |
| 42 | + |
| 43 | +### Purpose |
| 44 | + |
| 45 | +This test suite validates the ACP Python SDK's functionality across two levels: |
| 46 | + |
| 47 | +- **Unit Tests** - Verify individual functions and classes in isolation |
| 48 | +- **Integration Tests** - Validate end-to-end functionality with real blockchain/API calls |
| 49 | + |
| 50 | +The test suite ensures code quality, prevents regressions, and provides confidence when shipping new features. |
| 51 | + |
| 52 | +## Running Tests |
| 53 | + |
| 54 | +Below are commands to run the test suites. |
| 55 | + |
| 56 | +### All Tests |
| 57 | + |
| 58 | +```bash |
| 59 | +poetry run pytest |
| 60 | +``` |
| 61 | + |
| 62 | +### Unit Tests Only |
| 63 | + |
| 64 | +```bash |
| 65 | +poetry run pytest tests/unit |
| 66 | +``` |
| 67 | + |
| 68 | +### Integration Tests Only |
| 69 | + |
| 70 | +```bash |
| 71 | +poetry run pytest tests/integration |
| 72 | +``` |
| 73 | + |
| 74 | +### Specific Test Files |
| 75 | + |
| 76 | +```bash |
| 77 | +poetry run pytest tests/unit/test_job.py |
| 78 | +``` |
| 79 | + |
| 80 | +### Run Tests with Verbose Output |
| 81 | + |
| 82 | +```bash |
| 83 | +poetry run pytest -v |
| 84 | +``` |
| 85 | + |
| 86 | +### Generate Coverage Report |
| 87 | + |
| 88 | +```bash |
| 89 | +# Terminal report with missing lines |
| 90 | +poetry run pytest --cov=virtuals_acp --cov-report=term-missing |
| 91 | + |
| 92 | +# HTML report (opens in browser) |
| 93 | +poetry run pytest --cov=virtuals_acp --cov-report=html |
| 94 | +open htmlcov/index.html |
| 95 | +``` |
| 96 | + |
| 97 | +### Run Tests by Marker |
| 98 | + |
| 99 | +```bash |
| 100 | +# Run only unit tests |
| 101 | +poetry run pytest -m unit |
| 102 | + |
| 103 | +# Run only integration tests |
| 104 | +poetry run pytest -m integration |
| 105 | + |
| 106 | +# Run tests that don't require network |
| 107 | +poetry run pytest -m "not requires_network" |
| 108 | +``` |
| 109 | + |
| 110 | +## How to Write Tests |
| 111 | + |
| 112 | +### Unit Tests |
| 113 | + |
| 114 | +Unit tests should be **isolated, fast, and deterministic**. These tests don't involve any on-chain activity or external dependencies. |
| 115 | + |
| 116 | +**Location**: `tests/unit/` |
| 117 | + |
| 118 | +**General Guidelines:** |
| 119 | + |
| 120 | +- No network calls |
| 121 | +- No blockchain interactions |
| 122 | +- External dependencies are mocked using `unittest.mock` or `pytest-mock` |
| 123 | +- No `.env` needed |
| 124 | + |
| 125 | +**Example Structure:** |
| 126 | + |
| 127 | +```python |
| 128 | +# test_job.py |
| 129 | +import pytest |
| 130 | +from unittest.mock import MagicMock |
| 131 | +from virtuals_acp.job import ACPJob |
| 132 | +from virtuals_acp.exceptions import ACPError |
| 133 | +from virtuals_acp.models import ACPJobPhase, PriceType |
| 134 | + |
| 135 | +class TestACPJob: |
| 136 | + """Test suite for ACPJob class""" |
| 137 | + |
| 138 | + @pytest.fixture |
| 139 | + def mock_acp_client(self): |
| 140 | + """Create a mock VirtualsACP client""" |
| 141 | + client = MagicMock() |
| 142 | + client.config.base_fare = Fare( |
| 143 | + contract_address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", |
| 144 | + decimals=6 |
| 145 | + ) |
| 146 | + return client |
| 147 | + |
| 148 | + @pytest.fixture |
| 149 | + def sample_job_data(self, mock_acp_client): |
| 150 | + """Sample job data for testing""" |
| 151 | + return { |
| 152 | + "acp_client": mock_acp_client, |
| 153 | + "id": 123, |
| 154 | + "client_address": "0x1111111111111111111111111111111111111111", |
| 155 | + "provider_address": "0x2222222222222222222222222222222222222222", |
| 156 | + "price": 100.0, |
| 157 | + "memos": [], |
| 158 | + "phase": ACPJobPhase.REQUEST, |
| 159 | + } |
| 160 | + |
| 161 | + class TestInitialization: |
| 162 | + """Test job initialization""" |
| 163 | + |
| 164 | + def test_should_initialize_with_all_parameters(self, sample_job_data): |
| 165 | + """Should create a job with valid parameters""" |
| 166 | + job = ACPJob.model_construct(**sample_job_data) |
| 167 | + |
| 168 | + assert job is not None |
| 169 | + assert job.id == 123 |
| 170 | + assert job.phase == ACPJobPhase.REQUEST |
| 171 | + |
| 172 | + def test_should_raise_error_for_invalid_parameters(self): |
| 173 | + """Should throw error for invalid parameters""" |
| 174 | + with pytest.raises(ACPError): |
| 175 | + ACPJob(invalid_param="value") |
| 176 | + |
| 177 | +# Mocking Examples |
| 178 | +def test_with_mocked_api_call(mocker): |
| 179 | + """Example of mocking API calls""" |
| 180 | + mock_response = MagicMock() |
| 181 | + mock_response.json.return_value = {"data": [{"id": 1}]} |
| 182 | + mocker.patch('requests.get', return_value=mock_response) |
| 183 | + |
| 184 | + result = fetch_jobs() |
| 185 | + assert len(result) == 1 |
| 186 | + |
| 187 | +def test_with_mocked_contract_client(): |
| 188 | + """Example of mocking contract client""" |
| 189 | + mock_client = MagicMock() |
| 190 | + mock_client.create_job.return_value = {"type": "CREATE_JOB"} |
| 191 | + |
| 192 | + result = mock_client.create_job("0xProvider", "0xEvaluator") |
| 193 | + assert result["type"] == "CREATE_JOB" |
| 194 | +``` |
| 195 | + |
| 196 | +**What to Test:** |
| 197 | + |
| 198 | +- Input Validation |
| 199 | +- Error Handling |
| 200 | +- Edge Cases |
| 201 | +- Business Logic |
| 202 | +- State Transitions |
| 203 | +- Helper Functions |
| 204 | + |
| 205 | +### Integration Tests |
| 206 | + |
| 207 | +Integration tests verify the SDK works correctly with external dependencies/services (blockchain, APIs). |
| 208 | + |
| 209 | +**Location**: `tests/integration/` |
| 210 | + |
| 211 | +**General Guidelines:** |
| 212 | + |
| 213 | +- Require `.env` to be defined |
| 214 | +- Make real network & blockchain calls |
| 215 | +- Test partial end-to-end functionality |
| 216 | +- Use longer timeouts |
| 217 | + |
| 218 | +**Environment Setup** |
| 219 | + |
| 220 | +1. Copy `.env.example` to `.env`: |
| 221 | + |
| 222 | +```bash |
| 223 | +cp tests/.env.example tests/.env |
| 224 | +``` |
| 225 | + |
| 226 | +2. Populate environment variables: |
| 227 | + |
| 228 | +```bash |
| 229 | +# tests/.env |
| 230 | +# General Variables |
| 231 | +WHITELISTED_WALLET_PRIVATE_KEY=0x<PRIVATE_KEY> |
| 232 | + |
| 233 | +# Seller Agent Variables |
| 234 | +SELLER_ENTITY_ID=<ENTITY_ID> |
| 235 | +SELLER_AGENT_WALLET_ADDRESS=<WALLET_ADDRESS> |
| 236 | + |
| 237 | +# Buyer Agent Variables |
| 238 | +BUYER_ENTITY_ID=<ENTITY_ID> |
| 239 | +BUYER_AGENT_WALLET_ADDRESS=<WALLET_ADDRESS> |
| 240 | +``` |
| 241 | + |
| 242 | +**Example Structure:** |
| 243 | + |
| 244 | +```python |
| 245 | +# test_integration_contract_client_v2.py |
| 246 | +import pytest |
| 247 | +import os |
| 248 | +from virtuals_acp.contract_clients.contract_client_v2 import ACPContractClientV2 |
| 249 | +from virtuals_acp.configs.configs import BASE_MAINNET_CONFIG |
| 250 | + |
| 251 | +class TestIntegrationACPContractClientV2: |
| 252 | + """Integration tests for ACPContractClientV2""" |
| 253 | + |
| 254 | + @pytest.fixture |
| 255 | + def wallet_private_key(self): |
| 256 | + """Get private key from environment""" |
| 257 | + return os.getenv("WHITELISTED_WALLET_PRIVATE_KEY") |
| 258 | + |
| 259 | + class TestInitialization: |
| 260 | + """Test client initialization with real network""" |
| 261 | + |
| 262 | + @pytest.mark.integration |
| 263 | + def test_should_initialize_client_successfully(self, wallet_private_key): |
| 264 | + """Should initialize client with real credentials""" |
| 265 | + client = ACPContractClientV2.build( |
| 266 | + wallet_private_key, |
| 267 | + os.getenv("SELLER_ENTITY_ID"), |
| 268 | + os.getenv("SELLER_AGENT_WALLET_ADDRESS"), |
| 269 | + BASE_MAINNET_CONFIG |
| 270 | + ) |
| 271 | + |
| 272 | + assert client is not None |
| 273 | + assert client.agent_wallet_address is not None |
| 274 | + assert client.config.chain_id == 8453 |
| 275 | +``` |
| 276 | + |
| 277 | +**Important Notes:** |
| 278 | + |
| 279 | +- Integration tests load environment variables from `tests/.env` |
| 280 | +- If `.env` is missing, integration tests are skipped with a warning |
| 281 | +- Ensure test wallets are funded on the corresponding network (testnet/mainnet) |
| 282 | +- Use appropriate timeouts for network operations (`timeout = 300` in pytest.ini) |
| 283 | + |
| 284 | +## Test Configuration |
| 285 | + |
| 286 | +### pytest.ini |
| 287 | + |
| 288 | +Key configuration options: |
| 289 | + |
| 290 | +```ini |
| 291 | +[pytest] |
| 292 | +testpaths = tests # Where to find tests |
| 293 | +python_files = test_*.py # Test file pattern |
| 294 | +python_classes = Test* # Test class pattern |
| 295 | +python_functions = test_* # Test function pattern |
| 296 | + |
| 297 | +# Markers for organizing tests |
| 298 | +markers = |
| 299 | + unit: Unit tests |
| 300 | + integration: Integration tests |
| 301 | + slow: Tests that take a long time |
| 302 | + requires_network: Tests requiring network |
| 303 | + requires_blockchain: Tests requiring blockchain |
| 304 | + |
| 305 | +# Timeout for tests (prevents hanging) |
| 306 | +timeout = 300 # 5 minutes |
| 307 | + |
| 308 | +# Coverage options (uncomment to enable) |
| 309 | +# addopts = --cov=virtuals_acp --cov-report=html |
| 310 | +``` |
| 311 | + |
| 312 | +### conftest.py |
| 313 | + |
| 314 | +The `conftest.py` file handles: |
| 315 | + |
| 316 | +- Loading `.env` variables automatically |
| 317 | +- Providing warnings if `.env` is missing |
| 318 | +- Shared fixtures across test files |
| 319 | + |
| 320 | +## Best Practices |
| 321 | + |
| 322 | +### Test Organization |
| 323 | + |
| 324 | +- **Group related tests** using nested classes |
| 325 | +- **Use descriptive test names** starting with `test_should_` |
| 326 | +- **One assertion concept per test** (avoid testing too much in one test) |
| 327 | +- **Use fixtures** for reusable test data and mocks |
| 328 | + |
| 329 | +### Mocking |
| 330 | + |
| 331 | +- Always use `pytest.fixture` for shared mocks |
| 332 | +- Clear mocks between tests using `mocker.reset_mock()` or fresh fixtures |
| 333 | +- Mock at the appropriate level (not too deep, not too shallow) |
| 334 | + |
| 335 | +### Coverage |
| 336 | + |
| 337 | +- Aim for >90% statement coverage |
| 338 | +- Aim for >80% branch coverage |
| 339 | +- 100% function coverage for public methods |
| 340 | +- Use `--cov-report=html` to identify uncovered lines |
| 341 | + |
| 342 | +### Running Tests in CI/CD |
| 343 | + |
| 344 | +Tests run automatically on: |
| 345 | + |
| 346 | +- Pull requests (unit tests only) |
| 347 | +- Pushes to main (all tests including integration) |
| 348 | + |
| 349 | +See `.github/workflows/ci-test.yml` for CI configuration. |
| 350 | + |
| 351 | +## Troubleshooting |
| 352 | + |
| 353 | +### Integration Tests Skipped |
| 354 | + |
| 355 | +If you see "Integration tests will be skipped", create `tests/.env` from `tests/.env.example`. |
| 356 | + |
| 357 | +### Import Errors |
| 358 | + |
| 359 | +Make sure to install dev dependencies: |
| 360 | + |
| 361 | +```bash |
| 362 | +poetry install |
| 363 | +``` |
| 364 | + |
| 365 | +### Coverage Not Working |
| 366 | + |
| 367 | +Ensure pytest-cov is installed: |
| 368 | + |
| 369 | +```bash |
| 370 | +poetry add --group dev pytest-cov |
| 371 | +``` |
0 commit comments