Simple pytest fixtures for Docker and Docker Compose based tests
npx @tessl/cli install tessl/pypi-pytest-docker@3.2.00
# pytest-docker
1
2
Simple pytest fixtures for Docker and Docker Compose based tests. This library simplifies integration testing with containerized services by providing automatic lifecycle management, port discovery, and service readiness checking.
3
4
## Package Information
5
6
- **Package Name**: pytest-docker
7
- **Package Type**: pypi
8
- **Language**: Python
9
- **Installation**: `pip install pytest-docker`
10
- **Plugin Entry Point**: `pytest11 = docker = pytest_docker`
11
12
## Core Imports
13
14
```python
15
import pytest_docker
16
```
17
18
For accessing fixtures directly (optional, fixtures are automatically available):
19
20
```python
21
from pytest_docker import (
22
docker_ip,
23
docker_services,
24
docker_compose_file,
25
docker_compose_project_name,
26
docker_compose_command,
27
docker_setup,
28
docker_cleanup,
29
Services
30
)
31
```
32
33
## Basic Usage
34
35
Create a `docker-compose.yml` file in your `tests` directory:
36
37
```yaml
38
version: '2'
39
services:
40
httpbin:
41
image: "kennethreitz/httpbin"
42
ports:
43
- "8000:80"
44
```
45
46
Write integration tests using the fixtures:
47
48
```python
49
import pytest
50
import requests
51
from requests.exceptions import ConnectionError
52
53
54
def is_responsive(url):
55
"""Check if service is responsive."""
56
try:
57
response = requests.get(url)
58
if response.status_code == 200:
59
return True
60
except ConnectionError:
61
return False
62
63
64
@pytest.fixture(scope="session")
65
def http_service(docker_ip, docker_services):
66
"""Ensure that HTTP service is up and responsive."""
67
# Get the host port for container port 80
68
port = docker_services.port_for("httpbin", 80)
69
url = "http://{}:{}".format(docker_ip, port)
70
71
# Wait for service to be ready
72
docker_services.wait_until_responsive(
73
timeout=30.0,
74
pause=0.1,
75
check=lambda: is_responsive(url)
76
)
77
return url
78
79
80
def test_status_code(http_service):
81
"""Test HTTP service returns expected status codes."""
82
status = 418
83
response = requests.get(http_service + "/status/{}".format(status))
84
assert response.status_code == status
85
```
86
87
## Capabilities
88
89
### Docker IP Discovery
90
91
Automatically determines the correct IP address for connecting to Docker containers based on your Docker configuration.
92
93
```python { .api }
94
@pytest.fixture(scope="session")
95
def docker_ip() -> str:
96
"""
97
Determine the IP address for TCP connections to Docker containers.
98
99
Returns:
100
str: IP address for Docker connections (usually "127.0.0.1")
101
"""
102
```
103
104
### Docker Compose Configuration
105
106
Configures Docker Compose command, file locations, and project naming for container management.
107
108
```python { .api }
109
@pytest.fixture(scope="session")
110
def docker_compose_command() -> str:
111
"""
112
Docker Compose command to use for container operations.
113
114
Returns:
115
str: Command name, default "docker compose" (Docker Compose V2)
116
"""
117
118
@pytest.fixture(scope="session")
119
def docker_compose_file(pytestconfig) -> Union[List[str], str]:
120
"""
121
Get absolute path to docker-compose.yml file(s).
122
123
Args:
124
pytestconfig: pytest configuration object
125
126
Returns:
127
Union[List[str], str]: Path or list of paths to compose files
128
"""
129
130
@pytest.fixture(scope="session")
131
def docker_compose_project_name() -> str:
132
"""
133
Generate unique project name for Docker Compose.
134
135
Returns:
136
str: Project name (default: "pytest{PID}")
137
"""
138
```
139
140
### Container Lifecycle Management
141
142
Controls Docker container startup and cleanup behavior with configurable commands.
143
144
```python { .api }
145
@pytest.fixture(scope="session")
146
def docker_setup() -> Union[List[str], str]:
147
"""
148
Get docker-compose commands for container setup.
149
150
Returns:
151
Union[List[str], str]: Setup commands (default: ["up --build -d"])
152
"""
153
154
@pytest.fixture(scope="session")
155
def docker_cleanup() -> Union[List[str], str]:
156
"""
157
Get docker-compose commands for container cleanup.
158
159
Returns:
160
Union[List[str], str]: Cleanup commands (default: ["down -v"])
161
"""
162
163
def get_setup_command() -> Union[List[str], str]:
164
"""
165
Get default setup command for Docker containers.
166
167
Returns:
168
Union[List[str], str]: Default setup command list
169
"""
170
171
def get_cleanup_command() -> Union[List[str], str]:
172
"""
173
Get default cleanup command for Docker containers.
174
175
Returns:
176
Union[List[str], str]: Default cleanup command list
177
"""
178
```
179
180
### Service Management
181
182
Main interface for interacting with running Docker services, including port discovery and readiness checking.
183
184
```python { .api }
185
@pytest.fixture(scope="session")
186
def docker_services(
187
docker_compose_command: str,
188
docker_compose_file: Union[List[str], str],
189
docker_compose_project_name: str,
190
docker_setup: Union[List[str], str],
191
docker_cleanup: Union[List[str], str]
192
) -> Iterator[Services]:
193
"""
194
Start Docker services and provide Services interface.
195
196
Automatically starts containers before tests and cleans up afterward.
197
198
Args:
199
docker_compose_command: Docker Compose command to use
200
docker_compose_file: Path(s) to docker-compose.yml file(s)
201
docker_compose_project_name: Project name for container isolation
202
docker_setup: Command(s) to run for container setup
203
docker_cleanup: Command(s) to run for container cleanup
204
205
Yields:
206
Services: Interface for service interaction
207
"""
208
```
209
210
### Service Interaction
211
212
The Services class provides methods for port discovery and service readiness verification.
213
214
```python { .api }
215
class Services:
216
"""Interface for interacting with Docker services."""
217
218
def port_for(self, service: str, container_port: int) -> int:
219
"""
220
Get host port mapped to container port for a service.
221
222
Args:
223
service: Name of the Docker service from docker-compose.yml
224
container_port: Port number inside the container
225
226
Returns:
227
int: Host port number that maps to the container port
228
229
Raises:
230
ValueError: If port mapping cannot be determined
231
"""
232
233
def wait_until_responsive(
234
self,
235
check: Any,
236
timeout: float,
237
pause: float,
238
clock: Any = timeit.default_timer
239
) -> None:
240
"""
241
Wait until a service responds or timeout is reached.
242
243
Args:
244
check: Function that returns True when service is ready
245
timeout: Maximum time to wait in seconds
246
pause: Time to wait between checks in seconds
247
clock: Timer function (default: timeit.default_timer)
248
249
Raises:
250
Exception: If timeout is reached before service responds
251
"""
252
```
253
254
### Command Execution
255
256
Low-level utilities for executing Docker Compose commands and shell operations.
257
258
```python { .api }
259
class DockerComposeExecutor:
260
"""Executes Docker Compose commands with proper file and project configuration."""
261
262
def __init__(
263
self,
264
compose_command: str,
265
compose_files: Union[List[str], str],
266
compose_project_name: str
267
):
268
"""
269
Initialize executor with Docker Compose configuration.
270
271
Args:
272
compose_command: Docker Compose command ("docker compose" or "docker-compose")
273
compose_files: Path or list of paths to compose files
274
compose_project_name: Project name for container isolation
275
"""
276
277
def execute(self, subcommand: str, **kwargs) -> bytes:
278
"""
279
Execute Docker Compose subcommand.
280
281
Args:
282
subcommand: Docker Compose subcommand (e.g., "up -d", "down")
283
**kwargs: Additional arguments passed to shell execution
284
285
Returns:
286
bytes: Command output
287
288
Raises:
289
Exception: If command fails with non-zero exit code
290
"""
291
292
def execute(
293
command: str,
294
success_codes: Iterable[int] = (0,),
295
ignore_stderr: bool = False
296
) -> bytes:
297
"""
298
Execute shell command with error handling.
299
300
Args:
301
command: Shell command to execute
302
success_codes: Acceptable exit codes (default: (0,))
303
ignore_stderr: Whether to ignore stderr output
304
305
Returns:
306
bytes: Command output
307
308
Raises:
309
Exception: If command returns unacceptable exit code
310
"""
311
312
def get_docker_ip() -> str:
313
"""
314
Determine Docker host IP from DOCKER_HOST environment variable.
315
316
Returns:
317
str: IP address for Docker connections
318
"""
319
320
def container_scope_fixture(request: FixtureRequest) -> Any:
321
"""
322
Get container scope from pytest request configuration.
323
324
Args:
325
request: pytest fixture request object
326
327
Returns:
328
Any: Container scope configuration
329
"""
330
331
def containers_scope(fixture_name: str, config: Config) -> Any:
332
"""
333
Determine container scope for fixtures based on pytest configuration.
334
335
Args:
336
fixture_name: Name of the fixture
337
config: pytest configuration object
338
339
Returns:
340
Any: Container scope for the fixture
341
"""
342
```
343
344
### Plugin Configuration
345
346
Configures pytest plugin integration and command-line options.
347
348
```python { .api }
349
def pytest_addoption(parser: pytest.Parser) -> None:
350
"""
351
Add pytest command-line options for Docker container configuration.
352
353
Args:
354
parser: pytest argument parser
355
"""
356
```
357
358
## Configuration Options
359
360
### Container Scope
361
362
Control fixture scope using the `--container-scope` command line option:
363
364
```bash
365
pytest --container-scope session # Default: containers shared across test session
366
pytest --container-scope module # New containers for each test module
367
pytest --container-scope class # New containers for each test class
368
pytest --container-scope function # New containers for each test function
369
```
370
371
### Docker Compose V1 Support
372
373
For legacy Docker Compose V1 (`docker-compose` command):
374
375
```python
376
@pytest.fixture(scope="session")
377
def docker_compose_command() -> str:
378
return "docker-compose"
379
```
380
381
Or install with V1 support:
382
383
```bash
384
pip install pytest-docker[docker-compose-v1]
385
```
386
387
### Custom Docker Compose File Location
388
389
Override the default location (`tests/docker-compose.yml`):
390
391
```python
392
import os
393
import pytest
394
395
@pytest.fixture(scope="session")
396
def docker_compose_file(pytestconfig):
397
return os.path.join(str(pytestconfig.rootdir), "custom", "docker-compose.yml")
398
```
399
400
### Multiple Compose Files
401
402
Use multiple compose files for complex configurations:
403
404
```python
405
@pytest.fixture(scope="session")
406
def docker_compose_file(pytestconfig):
407
return [
408
os.path.join(str(pytestconfig.rootdir), "tests", "compose.yml"),
409
os.path.join(str(pytestconfig.rootdir), "tests", "compose.override.yml"),
410
]
411
```
412
413
### Custom Project Name
414
415
Pin project name to avoid conflicts during debugging:
416
417
```python
418
@pytest.fixture(scope="session")
419
def docker_compose_project_name() -> str:
420
return "my-test-project"
421
```
422
423
### Custom Setup and Cleanup
424
425
Modify container lifecycle commands:
426
427
```python
428
@pytest.fixture(scope="session")
429
def docker_setup():
430
return ["down -v", "up --build -d"] # Cleanup first, then start
431
432
@pytest.fixture(scope="session")
433
def docker_cleanup():
434
return ["down -v", "system prune -f"] # Extended cleanup
435
```
436
437
## Types
438
439
```python { .api }
440
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
441
from _pytest.config import Config
442
from _pytest.fixtures import FixtureRequest
443
import timeit
444
import pytest
445
446
# Type aliases for clarity
447
PortMapping = Dict[str, Dict[int, int]] # Service name -> {container_port: host_port}
448
```
449
450
## Error Handling
451
452
Common exceptions and error patterns:
453
454
- **ValueError**: Raised by `Services.port_for()` when port mapping cannot be determined
455
- **Exception**: Raised by `Services.wait_until_responsive()` when timeout is reached
456
- **Exception**: Raised by `execute()` functions when shell commands fail
457
- **subprocess.CalledProcessError**: Underlying exception for command execution failures
458
459
## Usage Patterns
460
461
### Database Testing
462
463
```python
464
@pytest.fixture(scope="session")
465
def postgres_service(docker_ip, docker_services):
466
"""Ensure PostgreSQL is ready for connections."""
467
port = docker_services.port_for("postgres", 5432)
468
469
def is_ready():
470
try:
471
conn = psycopg2.connect(
472
host=docker_ip,
473
port=port,
474
user="test",
475
password="test",
476
database="testdb"
477
)
478
conn.close()
479
return True
480
except psycopg2.OperationalError:
481
return False
482
483
docker_services.wait_until_responsive(
484
timeout=60.0,
485
pause=1.0,
486
check=is_ready
487
)
488
489
return {
490
"host": docker_ip,
491
"port": port,
492
"user": "test",
493
"password": "test",
494
"database": "testdb"
495
}
496
```
497
498
### API Testing
499
500
```python
501
@pytest.fixture(scope="session")
502
def api_service(docker_ip, docker_services):
503
"""Ensure API service is ready."""
504
port = docker_services.port_for("api", 3000)
505
url = f"http://{docker_ip}:{port}"
506
507
docker_services.wait_until_responsive(
508
timeout=30.0,
509
pause=0.5,
510
check=lambda: requests.get(f"{url}/health").status_code == 200
511
)
512
513
return url
514
```
515
516
### Multi-Service Testing
517
518
```python
519
@pytest.fixture(scope="session")
520
def full_stack(docker_ip, docker_services):
521
"""Ensure all services are ready."""
522
# Wait for database
523
db_port = docker_services.port_for("database", 5432)
524
# Wait for cache
525
cache_port = docker_services.port_for("redis", 6379)
526
# Wait for API (depends on db and cache)
527
api_port = docker_services.port_for("api", 8000)
528
529
# Check services in dependency order
530
docker_services.wait_until_responsive(
531
timeout=60.0, pause=1.0,
532
check=lambda: check_postgres(docker_ip, db_port)
533
)
534
535
docker_services.wait_until_responsive(
536
timeout=30.0, pause=0.5,
537
check=lambda: check_redis(docker_ip, cache_port)
538
)
539
540
docker_services.wait_until_responsive(
541
timeout=45.0, pause=1.0,
542
check=lambda: check_api_health(docker_ip, api_port)
543
)
544
545
return {
546
"database": {"host": docker_ip, "port": db_port},
547
"cache": {"host": docker_ip, "port": cache_port},
548
"api": {"host": docker_ip, "port": api_port}
549
}
550
```