| name | integration-testing |
| description | Use when setting up, running, or debugging integration tests against real Apple Contacts. Also use when unit tests pass but behavior seems wrong, when adding new AppleScript operations, or when you need to understand why mocked tests are insufficient for this project. |
Apple Contacts Integration Testing
Why Integration Tests Matter
Unit tests mock _run_applescript() and test Python logic only. They CANNOT catch:
- AppleScript syntax errors
- Variable naming conflicts in AppleScript
- Contacts.app API behavior differences between versions
- Silently-dropped record keys from NSJSONSerialization (e.g.,
name, id, size selector collisions)
- Gmail-specific behavior differences
- Timeout issues with real mailbox sizes
The OmniFocus project's story: A variable naming typo went undetected by 400+ unit tests because they all mocked the AppleScript boundary. Only integration tests against the real app caught it. This lesson applies equally to Apple Contacts.
Three-Tier Testing Strategy
| Tier | Speed | What it catches | When to run |
|---|
| Unit (mocked) | ~1s, 99 tests | Python logic, parsing, validation | Every change |
| Integration (real) | ~30s | AppleScript bugs, Contacts.app quirks | New AppleScript code |
| E2E (full MCP) | ~30s | Tool registration, parameter passing | New/modified tools |
Setting Up Integration Tests
Prerequisites
- Apple Contacts configured with at least one account
- macOS Automation permission granted to Terminal/IDE
Test Account Setup
export CONTACTS_TEST_GROUP="Gmail"
make test-integration
Running Tests
pytest tests/integration/ --run-integration -v
make test-integration
Writing Integration Tests
import pytest
from apple_contacts_mcp.contacts_connector import AppleContactsConnector
pytestmark = pytest.mark.skipif(
"not config.getoption('--run-integration')",
reason="Integration tests disabled by default."
)
class TestMailIntegration:
@pytest.fixture
def connector(self) -> AppleContactsConnector:
return AppleContactsConnector()
@pytest.fixture
def test_account(self) -> str:
import os
return os.getenv("CONTACTS_TEST_GROUP", "Gmail")
def test_list_mailboxes(self, connector, test_account):
"""Verify we can list mailboxes from a real account."""
result = connector.list_mailboxes(test_account)
assert isinstance(result, list)
assert len(result) > 0
assert any("INBOX" in mb for mb in result)
@pytest.mark.skip(reason="Sends real email - enable manually")
def test_send_email(self, connector):
"""Test sending real email - enable manually only."""
...
Key Patterns
Never Auto-Run Destructive Tests
Test Account Configuration
@pytest.fixture
def test_account(self) -> str:
"""Configurable test account - never hardcode."""
import os
return os.getenv("CONTACTS_TEST_GROUP", "Gmail")
Cleanup After Tests
@pytest.fixture
def test_mailbox(self, connector, test_account):
name = f"MCP-Test-{uuid.uuid4()}"
connector.create_mailbox(test_account, name)
yield name
try:
connector.delete_mailbox(test_account, name)
except Exception:
pass
Hard Rule
If you wrote or modified AppleScript in contacts_connector.py, integration tests must cover that operation before merge.
This is not optional. This is not "nice to have." Unit tests with mocked _run_applescript() give false confidence about AppleScript correctness.
Common Integration Test Failures
- "Not authorized to send Apple events" — Grant Automation permission in System Settings > Privacy & Security > Automation
- Account not found — Verify
CONTACTS_TEST_GROUP matches an actual configured account name in Contacts.app
- Timeout — Large mailboxes may exceed 60s default. Use
AppleContactsConnector(timeout=120)
- Gmail behavior — Gmail move/delete may behave differently than IMAP accounts. Test both if possible.