or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

attachments.mdcli.mdcomments.mdcore-api.mdenterprise.mdformulas.mdindex.mdorm.mdrecord-operations.mdtesting.mdwebhooks.md

testing.mddocs/

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

```