or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.md

index.mddocs/

0

# 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

```