0
# Testing Utilities
1
2
Mock Airtable APIs and helper functions for unit testing applications that use pyAirtable. Provides fake data generation, API simulation, and testing infrastructure without making real API calls.
3
4
## Capabilities
5
6
### Mock Airtable API
7
8
Context manager that intercepts pyAirtable API calls and provides controllable mock responses for testing.
9
10
```python { .api }
11
class MockAirtable:
12
def __init__(self, passthrough: bool = False):
13
"""
14
Initialize mock Airtable instance.
15
16
Parameters:
17
- passthrough: If True, unmocked methods make real requests
18
"""
19
20
def __enter__(self) -> 'MockAirtable':
21
"""Enter context manager and start mocking."""
22
23
def __exit__(self, *exc_info) -> None:
24
"""Exit context manager and stop mocking."""
25
26
def add_records(self, base_id: str, table_name: str, records: list[dict]) -> list[dict]:
27
"""
28
Add mock records to table.
29
30
Parameters:
31
- base_id: Base ID for records
32
- table_name: Table name for records
33
- records: List of record dicts or field dicts
34
35
Returns:
36
List of normalized record dicts with IDs
37
"""
38
39
def set_records(self, base_id: str, table_name: str, records: list[dict]) -> None:
40
"""
41
Replace all mock records for table.
42
43
Parameters:
44
- base_id: Base ID
45
- table_name: Table name
46
- records: List of record dicts to set
47
"""
48
49
def clear(self) -> None:
50
"""Clear all mock records."""
51
52
def set_passthrough(self, allowed: bool):
53
"""Context manager to temporarily enable/disable passthrough."""
54
55
def enable_passthrough(self):
56
"""Enable passthrough for real API calls."""
57
58
def disable_passthrough(self):
59
"""Disable passthrough (mock only)."""
60
61
def fake_record(fields: Optional[dict] = None, id: Optional[str] = None, **other_fields) -> dict:
62
"""
63
Generate fake record dict with proper structure.
64
65
Parameters:
66
- fields: Field values dict
67
- id: Record ID (auto-generated if not provided)
68
- other_fields: Additional field values as kwargs
69
70
Returns:
71
Complete record dict with id, createdTime, and fields
72
"""
73
74
def fake_user(value=None) -> dict:
75
"""
76
Generate fake user/collaborator dict.
77
78
Parameters:
79
- value: Name/identifier for user (auto-generated if not provided)
80
81
Returns:
82
User dict with id, email, and name
83
"""
84
85
def fake_attachment(url: str = "", filename: str = "") -> dict:
86
"""
87
Generate fake attachment dict.
88
89
Parameters:
90
- url: Attachment URL (auto-generated if not provided)
91
- filename: Filename (derived from URL if not provided)
92
93
Returns:
94
Attachment dict with id, url, filename, size, and type
95
"""
96
97
def fake_id(type: str = "rec", value=None) -> str:
98
"""
99
Generate fake Airtable-style ID.
100
101
Parameters:
102
- type: ID prefix (rec, app, tbl, etc.)
103
- value: Custom value to embed in ID
104
105
Returns:
106
Airtable-formatted ID string
107
"""
108
```
109
110
### Usage Examples
111
112
#### Basic Mock Setup
113
114
```python
115
from pyairtable import Api
116
from pyairtable.testing import MockAirtable, fake_record
117
118
def test_record_operations():
119
# Mock all pyAirtable API calls
120
with MockAirtable() as mock:
121
# Add test records
122
mock.add_records('app123', 'Contacts', [
123
{'Name': 'John Doe', 'Email': 'john@example.com'},
124
{'Name': 'Jane Smith', 'Email': 'jane@example.com'}
125
])
126
127
# Use normal pyAirtable code - no real API calls made
128
api = Api('fake_token')
129
table = api.table('app123', 'Contacts')
130
131
# Get all records (returns mock data)
132
records = table.all()
133
assert len(records) == 2
134
assert records[0]['fields']['Name'] == 'John Doe'
135
136
# Create record (adds to mock data)
137
new_record = table.create({'Name': 'Bob Wilson', 'Email': 'bob@example.com'})
138
assert new_record['id'].startswith('rec')
139
140
# Verify creation
141
all_records = table.all()
142
assert len(all_records) == 3
143
```
144
145
#### Pytest Integration
146
147
```python
148
import pytest
149
from pyairtable.testing import MockAirtable
150
151
@pytest.fixture(autouse=True)
152
def mock_airtable():
153
"""Auto-use fixture that mocks Airtable for all tests."""
154
with MockAirtable() as mock:
155
yield mock
156
157
def test_my_function(mock_airtable):
158
# Add test data
159
mock_airtable.add_records('base_id', 'table_name', [
160
{'Status': 'Active', 'Count': 5},
161
{'Status': 'Inactive', 'Count': 2}
162
])
163
164
# Test your function that uses pyAirtable
165
result = my_function_that_uses_airtable()
166
assert result == expected_value
167
```
168
169
#### Advanced Testing Scenarios
170
171
```python
172
from pyairtable.testing import MockAirtable, fake_record, fake_user, fake_attachment
173
174
def test_complex_data_structures():
175
with MockAirtable() as mock:
176
# Create records with various field types
177
test_records = [
178
fake_record({
179
'Name': 'Project Alpha',
180
'Status': 'In Progress',
181
'Collaborators': [fake_user('Alice'), fake_user('Bob')],
182
'Attachments': [
183
fake_attachment('https://example.com/doc.pdf', 'project_spec.pdf'),
184
fake_attachment('https://example.com/img.png', 'mockup.png')
185
],
186
'Priority': 5,
187
'Active': True
188
}, id='rec001'),
189
190
fake_record({
191
'Name': 'Project Beta',
192
'Status': 'Complete',
193
'Collaborators': [fake_user('Charlie')],
194
'Priority': 3,
195
'Active': False
196
}, id='rec002')
197
]
198
199
mock.set_records('app123', 'Projects', test_records)
200
201
# Test filtering and retrieval
202
api = Api('test_token')
203
table = api.table('app123', 'Projects')
204
205
active_projects = table.all(formula="{Active} = TRUE()")
206
assert len(active_projects) == 1
207
assert active_projects[0]['fields']['Name'] == 'Project Alpha'
208
```
209
210
#### Testing Error Conditions
211
212
```python
213
import requests
214
from pyairtable.testing import MockAirtable
215
216
def test_api_error_handling():
217
with MockAirtable(passthrough=False) as mock:
218
# Add limited test data
219
mock.add_records('app123', 'table1', [{'Name': 'Test'}])
220
221
api = Api('test_token')
222
table = api.table('app123', 'table1')
223
224
# This works - record exists in mock
225
record = table.get('rec000000000000001')
226
assert record['fields']['Name'] == 'Test'
227
228
# This raises KeyError - record not in mock data
229
with pytest.raises(KeyError):
230
table.get('rec999999999999999')
231
232
def test_with_real_api_fallback():
233
"""Test that combines mocking with real API calls."""
234
with MockAirtable() as mock:
235
# Enable passthrough for schema calls
236
with mock.enable_passthrough():
237
# This makes a real API call (if needed)
238
api = Api(os.environ['AIRTABLE_API_KEY'])
239
base = api.base('real_base_id')
240
schema = base.schema() # Real API call
241
242
# Back to mocking for data operations
243
mock.add_records('real_base_id', 'real_table', [
244
{'Field1': 'Mock Data'}
245
])
246
247
table = base.table('real_table')
248
records = table.all() # Uses mock data
249
assert records[0]['fields']['Field1'] == 'Mock Data'
250
```
251
252
#### Batch Operation Testing
253
254
```python
255
def test_batch_operations():
256
with MockAirtable() as mock:
257
# Test batch create
258
api = Api('test_token')
259
table = api.table('app123', 'Contacts')
260
261
batch_data = [
262
{'Name': f'User {i}', 'Email': f'user{i}@example.com'}
263
for i in range(1, 6)
264
]
265
266
created = table.batch_create(batch_data)
267
assert len(created) == 5
268
269
# Verify all records exist
270
all_records = table.all()
271
assert len(all_records) == 5
272
273
# Test batch update
274
updates = [
275
{'id': created[0]['id'], 'fields': {'Status': 'VIP'}},
276
{'id': created[1]['id'], 'fields': {'Status': 'Regular'}}
277
]
278
279
updated = table.batch_update(updates)
280
assert len(updated) == 2
281
assert updated[0]['fields']['Status'] == 'VIP'
282
283
# Test batch upsert
284
upsert_data = [
285
{'Name': 'User 1', 'Email': 'user1@example.com', 'Status': 'Premium'}, # Update
286
{'Name': 'User 6', 'Email': 'user6@example.com', 'Status': 'New'} # Create
287
]
288
289
result = table.batch_upsert(upsert_data, key_fields=['Name'])
290
assert len(result['updatedRecords']) == 1
291
assert len(result['createdRecords']) == 1
292
```
293
294
#### ORM Testing
295
296
```python
297
from pyairtable.orm import Model, fields
298
from pyairtable.testing import MockAirtable, fake_meta
299
300
class TestContact(Model):
301
# Use fake_meta for testing
302
Meta = fake_meta(
303
base_id='app123',
304
table_name='Contacts',
305
api_key='test_token'
306
)
307
308
name = fields.TextField('Name')
309
email = fields.EmailField('Email')
310
active = fields.CheckboxField('Active')
311
312
def test_orm_with_mock():
313
with MockAirtable() as mock:
314
# Create and save model instance
315
contact = TestContact(
316
name='John Doe',
317
email='john@example.com',
318
active=True
319
)
320
321
result = contact.save()
322
assert result.created is True
323
assert contact.name == 'John Doe'
324
325
# Test retrieval
326
all_contacts = TestContact.all()
327
assert len(all_contacts) == 1
328
assert all_contacts[0].name == 'John Doe'
329
330
# Test update
331
contact.active = False
332
result = contact.save()
333
assert result.created is False # Update, not create
334
```