mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 18:35:14 +00:00
* Add Dataverse SDK for Python: 5 new instruction files (error handling, authentication, performance, testing, use cases) + 4 prompts and updated READMEs * Delete COLLECTION_STATUS.md * Delete ENHANCEMENT_SUMMARY.md
487 lines
11 KiB
Markdown
487 lines
11 KiB
Markdown
---
|
|
applyTo: '**'
|
|
---
|
|
|
|
# Dataverse SDK for Python — Testing & Debugging Strategies
|
|
|
|
Based on official Azure Functions and pytest testing patterns.
|
|
|
|
## 1. Testing Overview
|
|
|
|
### Testing Pyramid for Dataverse SDK
|
|
|
|
```
|
|
Integration Tests <- Test with real Dataverse
|
|
/\
|
|
/ \
|
|
/Unit Tests (Mocked)\
|
|
/____________________\
|
|
< Framework Tests
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Unit Testing with Mocking
|
|
|
|
### Setup Test Environment
|
|
|
|
```bash
|
|
# Install test dependencies
|
|
pip install pytest pytest-cov unittest-mock
|
|
```
|
|
|
|
### Mock DataverseClient
|
|
|
|
```python
|
|
# tests/test_operations.py
|
|
import pytest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from PowerPlatform.Dataverse.client import DataverseClient
|
|
|
|
@pytest.fixture
|
|
def mock_client():
|
|
"""Provide mocked DataverseClient."""
|
|
client = Mock(spec=DataverseClient)
|
|
return client
|
|
|
|
def test_create_account(mock_client):
|
|
"""Test account creation with mocked client."""
|
|
|
|
# Setup mock response
|
|
mock_client.create.return_value = ["id-123"]
|
|
|
|
# Call function
|
|
from my_app import create_account
|
|
result = create_account(mock_client, {"name": "Acme"})
|
|
|
|
# Verify
|
|
assert result == "id-123"
|
|
mock_client.create.assert_called_once_with("account", {"name": "Acme"})
|
|
|
|
def test_create_account_error(mock_client):
|
|
"""Test error handling in account creation."""
|
|
from PowerPlatform.Dataverse.core.errors import DataverseError
|
|
|
|
# Setup mock to raise error
|
|
mock_client.create.side_effect = DataverseError(
|
|
message="Account exists",
|
|
code="validation_error",
|
|
status_code=400
|
|
)
|
|
|
|
# Verify error is raised
|
|
from my_app import create_account
|
|
with pytest.raises(DataverseError):
|
|
create_account(mock_client, {"name": "Acme"})
|
|
```
|
|
|
|
### Test Data Structures
|
|
|
|
```python
|
|
# tests/fixtures.py
|
|
import pytest
|
|
|
|
@pytest.fixture
|
|
def sample_account():
|
|
"""Sample account record for testing."""
|
|
return {
|
|
"accountid": "id-123",
|
|
"name": "Acme Inc",
|
|
"telephone1": "555-0100",
|
|
"statecode": 0,
|
|
"createdon": "2025-01-01T00:00:00Z"
|
|
}
|
|
|
|
@pytest.fixture
|
|
def sample_accounts(sample_account):
|
|
"""Multiple sample accounts."""
|
|
return [
|
|
sample_account,
|
|
{**sample_account, "accountid": "id-124", "name": "Fabrikam"},
|
|
{**sample_account, "accountid": "id-125", "name": "Contoso"},
|
|
]
|
|
|
|
# Usage in tests
|
|
def test_process_accounts(mock_client, sample_accounts):
|
|
mock_client.get.return_value = iter([sample_accounts])
|
|
# Test processing
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Mocking Common Patterns
|
|
|
|
### Mock Get with Pagination
|
|
|
|
```python
|
|
def test_pagination(mock_client, sample_accounts):
|
|
"""Test handling paginated results."""
|
|
|
|
# Mock returns generator with pages
|
|
mock_client.get.return_value = iter([
|
|
sample_accounts[:2], # Page 1
|
|
sample_accounts[2:] # Page 2
|
|
])
|
|
|
|
from my_app import process_all_accounts
|
|
result = process_all_accounts(mock_client)
|
|
|
|
assert len(result) == 3 # All pages processed
|
|
```
|
|
|
|
### Mock Bulk Operations
|
|
|
|
```python
|
|
def test_bulk_create(mock_client):
|
|
"""Test bulk account creation."""
|
|
|
|
payloads = [
|
|
{"name": "Account 1"},
|
|
{"name": "Account 2"},
|
|
]
|
|
|
|
# Mock returns list of IDs
|
|
mock_client.create.return_value = ["id-1", "id-2"]
|
|
|
|
from my_app import create_accounts
|
|
ids = create_accounts(mock_client, payloads)
|
|
|
|
assert len(ids) == 2
|
|
mock_client.create.assert_called_once_with("account", payloads)
|
|
```
|
|
|
|
### Mock Errors
|
|
|
|
```python
|
|
def test_rate_limiting_retry(mock_client):
|
|
"""Test retry logic on rate limiting."""
|
|
from PowerPlatform.Dataverse.core.errors import DataverseError
|
|
|
|
# Mock fails then succeeds
|
|
error = DataverseError(
|
|
message="Too many requests",
|
|
code="http_error",
|
|
status_code=429,
|
|
is_transient=True
|
|
)
|
|
mock_client.create.side_effect = [error, ["id-123"]]
|
|
|
|
from my_app import create_with_retry
|
|
result = create_with_retry(mock_client, "account", {})
|
|
|
|
assert result == "id-123"
|
|
assert mock_client.create.call_count == 2 # Retried
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Integration Testing
|
|
|
|
### Local Development Testing
|
|
|
|
```python
|
|
# tests/test_integration.py
|
|
import pytest
|
|
from azure.identity import InteractiveBrowserCredential
|
|
from PowerPlatform.Dataverse.client import DataverseClient
|
|
|
|
@pytest.fixture
|
|
def dataverse_client():
|
|
"""Real client for integration testing."""
|
|
client = DataverseClient(
|
|
base_url="https://myorg-dev.crm.dynamics.com",
|
|
credential=InteractiveBrowserCredential()
|
|
)
|
|
return client
|
|
|
|
@pytest.mark.integration
|
|
def test_create_and_retrieve_account(dataverse_client):
|
|
"""Test creating and retrieving account (against real Dataverse)."""
|
|
|
|
# Create
|
|
account_id = dataverse_client.create("account", {
|
|
"name": "Test Account"
|
|
})[0]
|
|
|
|
# Retrieve
|
|
account = dataverse_client.get("account", account_id)
|
|
|
|
# Verify
|
|
assert account["name"] == "Test Account"
|
|
|
|
# Cleanup
|
|
dataverse_client.delete("account", account_id)
|
|
```
|
|
|
|
### Test Isolation
|
|
|
|
```python
|
|
# tests/conftest.py
|
|
import pytest
|
|
|
|
@pytest.fixture(scope="function")
|
|
def test_account(dataverse_client):
|
|
"""Create test account, cleanup after test."""
|
|
|
|
account_id = dataverse_client.create("account", {
|
|
"name": "Test Account"
|
|
})[0]
|
|
|
|
yield account_id
|
|
|
|
# Cleanup
|
|
try:
|
|
dataverse_client.delete("account", account_id)
|
|
except:
|
|
pass # Already deleted
|
|
|
|
# Usage
|
|
def test_update_account(dataverse_client, test_account):
|
|
"""Test updating account."""
|
|
dataverse_client.update("account", test_account, {"telephone1": "555-0100"})
|
|
|
|
account = dataverse_client.get("account", test_account)
|
|
assert account["telephone1"] == "555-0100"
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Pytest Configuration
|
|
|
|
### pytest.ini
|
|
|
|
```ini
|
|
[pytest]
|
|
# Skip integration tests by default
|
|
testpaths = tests
|
|
python_files = test_*.py
|
|
python_classes = Test*
|
|
python_functions = test_*
|
|
|
|
markers =
|
|
integration: marks tests as integration (run with -m integration)
|
|
slow: marks tests as slow
|
|
unit: marks tests as unit tests
|
|
```
|
|
|
|
### Run Tests
|
|
|
|
```bash
|
|
# Unit tests only
|
|
pytest
|
|
|
|
# Unit + integration
|
|
pytest -m "unit or integration"
|
|
|
|
# Integration only
|
|
pytest -m integration
|
|
|
|
# With coverage
|
|
pytest --cov=my_app tests/
|
|
|
|
# Specific test
|
|
pytest tests/test_operations.py::test_create_account
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Coverage Analysis
|
|
|
|
### Generate Coverage Report
|
|
|
|
```bash
|
|
# Run tests with coverage
|
|
pytest --cov=my_app --cov-report=html tests/
|
|
|
|
# View coverage
|
|
open htmlcov/index.html # macOS
|
|
start htmlcov/index.html # Windows
|
|
```
|
|
|
|
### Coverage Configuration (.coveragerc)
|
|
|
|
```ini
|
|
[run]
|
|
branch = True
|
|
source = my_app
|
|
|
|
[report]
|
|
exclude_lines =
|
|
pragma: no cover
|
|
def __repr__
|
|
raise AssertionError
|
|
raise NotImplementedError
|
|
if __name__ == .__main__.:
|
|
|
|
[html]
|
|
directory = htmlcov
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Debugging with print/logging
|
|
|
|
### Enable Debug Logging
|
|
|
|
```python
|
|
import logging
|
|
import sys
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.StreamHandler(sys.stdout),
|
|
logging.FileHandler('debug.log')
|
|
]
|
|
)
|
|
|
|
# Enable SDK logging
|
|
logging.getLogger('PowerPlatform').setLevel(logging.DEBUG)
|
|
logging.getLogger('azure').setLevel(logging.DEBUG)
|
|
|
|
# In test
|
|
def test_with_logging(mock_client):
|
|
logger = logging.getLogger(__name__)
|
|
logger.debug("Starting test")
|
|
|
|
result = my_function(mock_client)
|
|
|
|
logger.debug(f"Result: {result}")
|
|
```
|
|
|
|
### Pytest Capturing Output
|
|
|
|
```bash
|
|
# Show print/logging output in tests
|
|
pytest -s tests/
|
|
|
|
# Capture and show on failure only
|
|
pytest --tb=short tests/
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Performance Testing
|
|
|
|
### Measure Operation Duration
|
|
|
|
```python
|
|
import pytest
|
|
import time
|
|
|
|
def test_bulk_create_performance(dataverse_client):
|
|
"""Test bulk create performance."""
|
|
|
|
payloads = [{"name": f"Account {i}"} for i in range(1000)]
|
|
|
|
start = time.time()
|
|
ids = dataverse_client.create("account", payloads)
|
|
duration = time.time() - start
|
|
|
|
assert len(ids) == 1000
|
|
assert duration < 10 # Should complete in under 10 seconds
|
|
|
|
print(f"Created 1000 records in {duration:.2f}s ({1000/duration:.0f} records/s)")
|
|
```
|
|
|
|
### Pytest Benchmark Plugin
|
|
|
|
```bash
|
|
pip install pytest-benchmark
|
|
```
|
|
|
|
```python
|
|
def test_query_performance(benchmark, dataverse_client):
|
|
"""Benchmark query performance."""
|
|
|
|
def get_accounts():
|
|
return list(dataverse_client.get("account", top=100))
|
|
|
|
result = benchmark(get_accounts)
|
|
assert len(result) <= 100
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Common Testing Patterns
|
|
|
|
### Testing Retry Logic
|
|
|
|
```python
|
|
def test_retry_on_transient_error(mock_client):
|
|
"""Test retry on transient error."""
|
|
from PowerPlatform.Dataverse.core.errors import DataverseError
|
|
|
|
error = DataverseError(
|
|
message="Timeout",
|
|
code="http_error",
|
|
status_code=408,
|
|
is_transient=True
|
|
)
|
|
|
|
# Fail then succeed
|
|
mock_client.create.side_effect = [error, ["id-123"]]
|
|
|
|
from my_app import create_with_retry
|
|
result = create_with_retry(mock_client, "account", {})
|
|
|
|
assert result == "id-123"
|
|
```
|
|
|
|
### Testing Filter Building
|
|
|
|
```python
|
|
def test_filter_builder():
|
|
"""Test OData filter generation."""
|
|
from my_app import build_account_filter
|
|
|
|
# Test cases
|
|
assert build_account_filter(status="active") == "statecode eq 0"
|
|
assert build_account_filter(name="Acme") == "contains(name, 'Acme')"
|
|
assert build_account_filter(status="active", name="Acme") \
|
|
== "statecode eq 0 and contains(name, 'Acme')"
|
|
```
|
|
|
|
### Testing Error Handling
|
|
|
|
```python
|
|
def test_handles_missing_record(mock_client):
|
|
"""Test handling 404 errors."""
|
|
from PowerPlatform.Dataverse.core.errors import DataverseError
|
|
|
|
mock_client.get.side_effect = DataverseError(
|
|
message="Not found",
|
|
code="http_error",
|
|
status_code=404
|
|
)
|
|
|
|
from my_app import get_account_safe
|
|
result = get_account_safe(mock_client, "invalid-id")
|
|
|
|
assert result is None # Returns None instead of raising
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Debugging Checklist
|
|
|
|
| Issue | Debug Steps |
|
|
|-------|-------------|
|
|
| Test fails unexpectedly | Add `-s` flag to see print output |
|
|
| Mock not called | Check method name/parameters match exactly |
|
|
| Real API failing | Check credentials, URL, permissions |
|
|
| Rate limiting in tests | Add delays or use smaller batches |
|
|
| Data not found | Verify record created and not cleaned up |
|
|
| Assertion errors | Print actual vs expected values |
|
|
|
|
---
|
|
|
|
## 11. See Also
|
|
|
|
- [Pytest Documentation](https://docs.pytest.org/)
|
|
- [unittest.mock Reference](https://docs.python.org/3/library/unittest.mock.html)
|
|
- [Azure Functions Testing](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python#unit-testing)
|
|
- [Dataverse SDK Examples](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/tree/main/examples)
|