mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 10:25:13 +00:00
Add Dataverse SDK for Python Collection (#458)
* 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
This commit is contained in:
committed by
GitHub
parent
b81a3dc5a4
commit
a89019fb3b
486
instructions/dataverse-python-testing-debugging.instructions.md
Normal file
486
instructions/dataverse-python-testing-debugging.instructions.md
Normal file
@@ -0,0 +1,486 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user