├── .github └── workflows │ ├── build-and-publish.yaml │ └── tests.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── pytest_docker │ ├── __init__.py │ ├── plugin.py │ └── py.typed └── tests ├── conftest.py ├── containers └── hello │ ├── Dockerfile │ └── server.py ├── docker-compose.yml ├── test_docker_ip.py ├── test_docker_services.py ├── test_dockercomposeexecutor.py ├── test_fixtures.py └── test_integration.py /.github/workflows/build-and-publish.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build and Publish 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '*.md' 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.9 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip build twine 22 | - name: Build distribution packages 23 | run: | 24 | python -m build 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | twine upload dist/* 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test Python package 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 11 | pytest-version: [4, 5, 6, 7, 8] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies, pytest ${{ matrix.pytest-version }} 20 | run: | 21 | python -m pip install --upgrade pip setuptools 22 | pip install pytest==${{ matrix.pytest-version }} 23 | pip install ".[tests]" 24 | - name: Build image 25 | run: | 26 | docker compose -f tests/docker-compose.yml pull 27 | docker compose -f tests/docker-compose.yml build 28 | - name: Run tests 29 | run: | 30 | pytest -c setup.cfg 31 | - name: Stop Docker compose 32 | run: | 33 | docker compose -f tests/docker-compose.yml down 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.pyc 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | .DS_Store 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | 128 | 129 | tags 130 | 131 | .vim/* 132 | */.vim/* 133 | *.swp 134 | *.swo 135 | 136 | #VS code 137 | .vscode 138 | 139 | #teamcity 140 | .teamcity/*.iml 141 | .teamcity/target/ 142 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 2.0.0 4 | 5 | Breaking changes: 6 | 7 | - Default command changed from `docker-compose` to `docker compose`, so the V2 8 | is the default one. 9 | 10 | ## Version 1.0.1 11 | 12 | Chore: 13 | 14 | - Set dependency on `attrs` to be the same as in `pytest` 15 | 16 | ## Version 1.0.0 17 | 18 | Breaking changes: 19 | 20 | - Default behavior is not to install `docker-compose` at all when 21 | installing `pytest-docker` with PIP. Use the `docker-compose-v1` extra. 22 | 23 | Feat: 24 | 25 | - Add support for Docker Compose v2 via a new pytest fixture, 26 | `docker_compose_command` that should return `docker compose`. 27 | 28 | ## Version 0.13.0 29 | 30 | Feat: 31 | 32 | - In get_docker_ip(), if `DOCKER_HOST` is using the `unix:` scheme then return "127.0.0.1" 33 | 34 | ## Version 0.12.0 35 | 36 | Changes: 37 | 38 | - Add `docker_setup` fixture to allow custom setup actions for docker-compose 39 | (contributed by @raddessi) 40 | 41 | ## Version 0.11.0 42 | 43 | Changes: 44 | 45 | - Add support for `pytest` v7 (contributed by @skshetry) 46 | 47 | ## Version 0.10.3 48 | 49 | Changes: 50 | 51 | - Ensure that Docker cleanup is executed even with after tests have failed 52 | 53 | ## Version 0.10.2 54 | 55 | Changes: 56 | 57 | - Allow higher version of `attrs` (21.0) 58 | 59 | ## Version 0.10.1 60 | 61 | Changes: 62 | 63 | - Allow higher version of `attrs` 64 | 65 | ## Version 0.10.0 66 | 67 | Changes: 68 | 69 | - Drop Python3.5 support 70 | 71 | ## Version 0.9.0 72 | 73 | Changes: 74 | 75 | - Add the `docker_cleanup` fixture to allow custom cleanup actions for 76 | docker-compose 77 | 78 | ## Version 0.8.0 79 | 80 | Changes: 81 | 82 | - Add explicit dependencies on `docker-compose` and `pytest` 83 | - Stop using deprecated `pytest-runner` to run package tests 84 | 85 | ## Version 0.7.2 86 | 87 | Changes: 88 | 89 | - Update package README 90 | 91 | ## Version 0.7.1 92 | 93 | - Minor packaging related fixes 94 | - Fix description content type in `setup.cfg` 95 | - Add `skip_cleanup` option to travis-ci settings 96 | 97 | ## Version 0.7.0 98 | 99 | - Removed the fallback method that allowed running without Docker 100 | - Package cleanup 101 | 102 | ## Version 0.6.0 103 | 104 | - Added ability to return list of files from `docker_compose_file` fixture 105 | 106 | ## Version 0.3.0 107 | 108 | - Added `--build` option to `docker-compose up` command to automatically 109 | rebuild local containers 110 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 pytest-docker contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Docker-based integration tests 2 | ===== 3 | 4 | [![PyPI version](https://img.shields.io/pypi/v/pytest-docker?color=green)](https://pypi.org/project/pytest-docker/) 5 | [![Build Status](https://github.com/avast/pytest-docker/actions/workflows/tests.yaml/badge.svg?branch=master)](https://github.com/avast/pytest-docker/actions/workflows/tests.yaml) 6 | [![Python versions](https://img.shields.io/pypi/pyversions/pytest-docker)](https://pypi.org/project/pytest-docker/) 7 | [![Code style](https://img.shields.io/badge/formatted%20with-black-black)](https://github.com/psf/black) 8 | 9 | # Description 10 | 11 | Simple [pytest](http://doc.pytest.org/) fixtures that help you write integration 12 | tests with Docker and [Docker Compose](https://docs.docker.com/compose/). 13 | Specify all necessary containers in a `docker-compose.yml` file and 14 | `pytest-docker` will spin them up for the duration of your tests. 15 | 16 | `pytest-docker` was originally created by André Caron. 17 | 18 | # Installation 19 | 20 | Install `pytest-docker` with `pip` or add it to your test requirements. 21 | 22 | By default, it uses the `docker compose` command, so it relies on the Compose plugin for Docker (also called Docker Compose V2). 23 | 24 | ## Docker Compose V1 compatibility 25 | 26 | If you want to use the old `docker-compose` command (deprecated since July 2023, not receiving updates since 2021) 27 | then you can do it using the [`docker-compose-command`](#docker_compose_command) fixture: 28 | 29 | ```python 30 | @pytest.fixture(scope="session") 31 | def docker_compose_command() -> str: 32 | return "docker-compose" 33 | ``` 34 | 35 | If you want to use the pip-distributed version of `docker-compose` command, you can install it using 36 | 37 | ``` 38 | pip install pytest-docker[docker-compose-v1] 39 | ``` 40 | 41 | Another option could be usage of [`compose-switch`](https://github.com/docker/compose-switch). 42 | 43 | # Usage 44 | 45 | Here is an example of a test that depends on an HTTP service. 46 | 47 | With a `docker-compose.yml` file like this (using the 48 | [httpbin](https://httpbin.org/) service): 49 | 50 | ```yaml 51 | version: '2' 52 | services: 53 | httpbin: 54 | image: "kennethreitz/httpbin" 55 | ports: 56 | - "8000:80" 57 | ``` 58 | 59 | You can write a test like this: 60 | 61 | ```python 62 | import pytest 63 | import requests 64 | 65 | from requests.exceptions import ConnectionError 66 | 67 | 68 | def is_responsive(url): 69 | try: 70 | response = requests.get(url) 71 | if response.status_code == 200: 72 | return True 73 | except ConnectionError: 74 | return False 75 | 76 | 77 | @pytest.fixture(scope="session") 78 | def http_service(docker_ip, docker_services): 79 | """Ensure that HTTP service is up and responsive.""" 80 | 81 | # `port_for` takes a container port and returns the corresponding host port 82 | port = docker_services.port_for("httpbin", 80) 83 | url = "http://{}:{}".format(docker_ip, port) 84 | docker_services.wait_until_responsive( 85 | timeout=30.0, pause=0.1, check=lambda: is_responsive(url) 86 | ) 87 | return url 88 | 89 | 90 | def test_status_code(http_service): 91 | status = 418 92 | response = requests.get(http_service + "/status/{}".format(status)) 93 | 94 | assert response.status_code == status 95 | ``` 96 | 97 | By default, this plugin will try to open `docker-compose.yml` in your 98 | `tests` directory. If you need to use a custom location, override the 99 | `docker_compose_file` fixture inside your `conftest.py` file: 100 | 101 | ```python 102 | import os 103 | import pytest 104 | 105 | 106 | @pytest.fixture(scope="session") 107 | def docker_compose_file(pytestconfig): 108 | return os.path.join(str(pytestconfig.rootdir), "mycustomdir", "docker-compose.yml") 109 | ``` 110 | 111 | To use [multiple compose files](https://docs.docker.com/compose/how-tos/multiple-compose-files/merge/), 112 | return a list of paths from the `docker_compose_file` fixture: 113 | 114 | ```python 115 | import os 116 | import pytest 117 | 118 | 119 | @pytest.fixture(scope="session") 120 | def docker_compose_file(pytestconfig): 121 | return [ 122 | os.path.join(str(pytestconfig.rootdir), "tests", "compose.yml"), 123 | os.path.join(str(pytestconfig.rootdir), "tests", "compose.override.yml"), 124 | ] 125 | ``` 126 | 127 | If you want to debug the test suite in your IDE (Pycharm, VsCode, etc.) and need to stop the test, the stack will be left running. 128 | To avoid creating multiple stacks, you can pin the project name and always teardown before starting a new stack: 129 | 130 | ```python 131 | import pytest 132 | 133 | # Pin the project name to avoid creating multiple stacks 134 | @pytest.fixture(scope="session") 135 | def docker_compose_project_name() -> str: 136 | return "my-compose-project" 137 | 138 | # Stop the stack before starting a new one 139 | @pytest.fixture(scope="session") 140 | def docker_setup(): 141 | return ["down -v", "up --build -d"] 142 | ``` 143 | 144 | 145 | ## Available fixtures 146 | 147 | By default, the scope of the fixtures are `session` but can be changed with 148 | `pytest` command line option `--container-scope `: 149 | 150 | ```bash 151 | pytest --container-scope 152 | ``` 153 | 154 | For available scopes and descriptions 155 | see 156 | 157 | ### `docker_ip` 158 | 159 | Determine the IP address for TCP connections to Docker containers. 160 | 161 | ### `docker_compose_file` 162 | 163 | Get an absolute path to the `docker-compose.yml` file. Override this fixture in 164 | your tests if you need a custom location. 165 | 166 | ### `docker_compose_project_name` 167 | 168 | Generate a project name using the current process PID. Override this fixture in 169 | your tests if you need a particular project name. 170 | 171 | ### `docker_services` 172 | 173 | Start all services from the docker compose file (`docker-compose up`). 174 | After test are finished, shutdown all services (`docker-compose down`). 175 | 176 | ### `docker_compose_command` 177 | 178 | Docker Compose command to use to execute Dockers. Default is to use 179 | Docker Compose V2 (command is `docker compose`). If you want to use 180 | Docker Compose V1, change this fixture to return `docker-compose`. 181 | 182 | ### `docker_setup` 183 | 184 | Get the list of docker_compose commands to be executed for test spawn actions. 185 | Override this fixture in your tests if you need to change spawn actions. 186 | Returning anything that would evaluate to False will skip this command. 187 | 188 | ### `docker_cleanup` 189 | 190 | Get the list of docker_compose commands to be executed for test clean-up actions. 191 | Override this fixture in your tests if you need to change clean-up actions. 192 | Returning anything that would evaluate to False will skip this command. 193 | 194 | # Development 195 | 196 | Use of a virtual environment is recommended. See the 197 | [venv](https://docs.python.org/3/library/venv.html) package for more 198 | information. 199 | 200 | First, install `pytest-docker` and its test dependencies: 201 | 202 | ```bash 203 | pip install -e ".[tests]" 204 | ``` 205 | 206 | Run tests with 207 | 208 | ```bash 209 | pytest -c setup.cfg 210 | ``` 211 | 212 | to make sure that the correct configuration is used. This is also how tests are 213 | run in CI. 214 | 215 | Use [black](https://pypi.org/project/black/) with default settings for 216 | formatting. You can also use `pylint` with `setup.cfg` as the configuration 217 | file as well as `mypy` for type checking. 218 | 219 | # Contributing 220 | 221 | This `pytest` plug-in and its source code are made available to you under a MIT 222 | license. It is safe to use in commercial and closed-source applications. Read 223 | the license for details! 224 | 225 | Found a bug? Think a new feature would make this plug-in more practical? We 226 | welcome issues and pull requests! 227 | 228 | When creating a pull request, be sure to follow this projects conventions (see 229 | above). 230 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pytest-docker 3 | version = 3.2.2 4 | description = Simple pytest fixtures for Docker and Docker Compose based tests 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | keywords = docker,docker-compose,pytest 8 | url = https://github.com/avast/pytest-docker 9 | 10 | author = Max K., Andre Caron 11 | author_email = maxim.kovykov@avast.com 12 | 13 | license = MIT 14 | classifiers = 15 | Programming Language :: Python :: 3 16 | Programming Language :: Python :: 3.8 17 | Programming Language :: Python :: 3.9 18 | Programming Language :: Python :: 3.10 19 | Programming Language :: Python :: 3.11 20 | Programming Language :: Python :: 3.12 21 | Programming Language :: Python :: 3.13 22 | License :: OSI Approved :: MIT License 23 | Topic :: Utilities 24 | Intended Audience :: Developers 25 | Operating System :: Unix 26 | Operating System :: POSIX 27 | Operating System :: Microsoft :: Windows 28 | 29 | [options] 30 | python_requires = >=3.8 31 | 32 | package_dir= 33 | =src 34 | 35 | packages=pytest_docker 36 | 37 | install_requires = 38 | pytest >=4.0, <9.0 39 | attrs >=19.2.0 40 | 41 | [options.extras_require] 42 | docker-compose-v1 = 43 | docker-compose >=1.27.3, <2.0 44 | tests = 45 | requests >=2.22.0, <3.0 46 | mypy >=0.500, <2.000 47 | pytest-pylint >=0.14.1, <1.0 48 | pytest-pycodestyle >=2.0.0, <3.0 49 | pytest-mypy >=0.10, <1.0 50 | types-requests >=2.31, <3.0 51 | types-setuptools >=69.0, <70.0 52 | 53 | [options.entry_points] 54 | pytest11 = 55 | docker = pytest_docker 56 | 57 | [tool:pytest] 58 | addopts = --verbose --mypy --pycodestyle --pylint-rcfile=setup.cfg --pylint 59 | 60 | # Configuration for pylint 61 | [pylint.MASTER] 62 | good-names = "logger,e,i,j,n,m,f,_" 63 | 64 | [pylint] 65 | disable = all 66 | enable = unused-import, 67 | fixme, 68 | useless-object-inheritance, 69 | unused-variable, 70 | unused-argument, 71 | unexpected-keyword-arg, 72 | string, 73 | unreachable, 74 | invalid-name, 75 | logging-not-lazy, 76 | unnecesary-pass, 77 | broad-except 78 | 79 | [pycodestyle] 80 | max-line-length=120 81 | ignore=E4,E7,W3 82 | 83 | [mypy] 84 | strict = true 85 | mypy_path = "src/pytest_docker,tests" 86 | namespace_packages = true 87 | warn_unused_ignores = true 88 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/pytest_docker/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .plugin import ( 4 | docker_cleanup, 5 | docker_compose_command, 6 | docker_compose_file, 7 | docker_compose_project_name, 8 | docker_ip, 9 | docker_services, 10 | docker_setup, 11 | Services, 12 | ) 13 | 14 | __all__ = [ 15 | "docker_compose_command", 16 | "docker_compose_file", 17 | "docker_compose_project_name", 18 | "docker_ip", 19 | "docker_setup", 20 | "docker_cleanup", 21 | "docker_services", 22 | "Services", 23 | ] 24 | 25 | 26 | def pytest_addoption(parser: pytest.Parser) -> None: 27 | group = parser.getgroup("docker") 28 | group.addoption( 29 | "--container-scope", 30 | type=str, 31 | action="store", 32 | default="session", 33 | help="The pytest fixture scope for reusing containers between tests." 34 | " For available scopes and descriptions, " 35 | " see https://docs.pytest.org/en/6.2.x/fixture.html#fixture-scopes", 36 | ) 37 | -------------------------------------------------------------------------------- /src/pytest_docker/plugin.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import re 4 | import subprocess 5 | import time 6 | import timeit 7 | from typing import Any, Dict, Iterable, Iterator, List, Tuple, Union 8 | 9 | import attr 10 | import pytest 11 | from _pytest.config import Config 12 | from _pytest.fixtures import FixtureRequest 13 | 14 | 15 | @pytest.fixture 16 | def container_scope_fixture(request: FixtureRequest) -> Any: 17 | return request.config.getoption("--container-scope") 18 | 19 | 20 | def containers_scope(fixture_name: str, config: Config) -> Any: # pylint: disable=unused-argument 21 | return config.getoption("--container-scope", "session") 22 | 23 | 24 | def execute(command: str, success_codes: Iterable[int] = (0,), ignore_stderr: bool = False) -> Union[bytes, Any]: 25 | """Run a shell command.""" 26 | try: 27 | stderr_pipe = subprocess.DEVNULL if ignore_stderr else subprocess.STDOUT 28 | output = subprocess.check_output(command, stderr=stderr_pipe, shell=True) 29 | status = 0 30 | except subprocess.CalledProcessError as error: 31 | output = error.output or b"" 32 | status = error.returncode 33 | command = error.cmd 34 | 35 | if status not in success_codes: 36 | raise Exception( 37 | 'Command {} returned {}: """{}""".'.format(command, status, output.decode("utf-8")) 38 | ) 39 | return output 40 | 41 | 42 | def get_docker_ip() -> Union[str, Any]: 43 | # When talking to the Docker daemon via a UNIX socket, route all TCP 44 | # traffic to docker containers via the TCP loopback interface. 45 | docker_host = os.environ.get("DOCKER_HOST", "").strip() 46 | if not docker_host or docker_host.startswith("unix://"): 47 | return "127.0.0.1" 48 | 49 | # Return just plain address without prefix and port 50 | return re.sub(r"^[^:]+://(.+):\d+$", r"\1", docker_host) 51 | 52 | 53 | @pytest.fixture(scope=containers_scope) 54 | def docker_ip() -> Union[str, Any]: 55 | """Determine the IP address for TCP connections to Docker containers.""" 56 | 57 | return get_docker_ip() 58 | 59 | 60 | @attr.s(frozen=True) 61 | class Services: 62 | _docker_compose: Any = attr.ib() 63 | _services: Dict[Any, Dict[Any, Any]] = attr.ib(init=False, default=attr.Factory(dict)) 64 | 65 | def port_for(self, service: str, container_port: int) -> int: 66 | """Return the "host" port for `service` and `container_port`. 67 | 68 | E.g. If the service is defined like this: 69 | 70 | version: '2' 71 | services: 72 | httpbin: 73 | build: . 74 | ports: 75 | - "8000:80" 76 | 77 | this method will return 8000 for container_port=80. 78 | """ 79 | 80 | # Lookup in the cache. 81 | cache: int = self._services.get(service, {}).get(container_port, None) 82 | if cache is not None: 83 | return cache 84 | 85 | output = self._docker_compose.execute("port %s %d" % (service, container_port)) 86 | endpoint = output.strip().decode("utf-8") 87 | if not endpoint: 88 | raise ValueError('Could not detect port for "%s:%d".' % (service, container_port)) 89 | 90 | # This handles messy output that might contain warnings or other text 91 | if len(endpoint.split("\n")) > 1: 92 | endpoint = endpoint.split("\n")[-1] 93 | 94 | # Usually, the IP address here is 0.0.0.0, so we don't use it. 95 | match = int(endpoint.split(":", 1)[-1]) 96 | 97 | # Store it in cache in case we request it multiple times. 98 | self._services.setdefault(service, {})[container_port] = match 99 | 100 | return match 101 | 102 | def wait_until_responsive( 103 | self, 104 | check: Any, 105 | timeout: float, 106 | pause: float, 107 | clock: Any = timeit.default_timer, 108 | ) -> None: 109 | """Wait until a service is responsive.""" 110 | 111 | ref = clock() 112 | now = ref 113 | while (now - ref) < timeout: 114 | if check(): 115 | return 116 | time.sleep(pause) 117 | now = clock() 118 | 119 | raise Exception("Timeout reached while waiting on service!") 120 | 121 | 122 | def str_to_list(arg: Union[str, List[Any], Tuple[Any]]) -> Union[List[Any], Tuple[Any]]: 123 | if isinstance(arg, (list, tuple)): 124 | return arg 125 | return [arg] 126 | 127 | 128 | @attr.s(frozen=True) 129 | class DockerComposeExecutor: 130 | _compose_command: str = attr.ib() 131 | _compose_files: Any = attr.ib(converter=str_to_list) 132 | _compose_project_name: str = attr.ib() 133 | 134 | def execute(self, subcommand: str, **kwargs: Any) -> Union[bytes, Any]: 135 | command = self._compose_command 136 | for compose_file in self._compose_files: 137 | command += ' -f "{}"'.format(compose_file) 138 | command += ' -p "{}" {}'.format(self._compose_project_name, subcommand) 139 | return execute(command, **kwargs) 140 | 141 | 142 | @pytest.fixture(scope=containers_scope) 143 | def docker_compose_command() -> str: 144 | """Docker Compose command to use, it could be either `docker compose` 145 | for Docker Compose V2 or `docker-compose` for Docker Compose 146 | V1.""" 147 | 148 | return "docker compose" 149 | 150 | 151 | @pytest.fixture(scope=containers_scope) 152 | def docker_compose_file(pytestconfig: Any) -> Union[List[str], str]: 153 | """Get an absolute path to the `docker-compose.yml` file. Override this 154 | fixture in your tests if you need a custom location.""" 155 | 156 | return os.path.join(str(pytestconfig.rootdir), "tests", "docker-compose.yml") 157 | 158 | 159 | @pytest.fixture(scope=containers_scope) 160 | def docker_compose_project_name() -> str: 161 | """Generate a project name using the current process PID. Override this 162 | fixture in your tests if you need a particular project name.""" 163 | 164 | return "pytest{}".format(os.getpid()) 165 | 166 | 167 | def get_cleanup_command() -> Union[List[str], str]: 168 | return ["down -v"] 169 | 170 | 171 | @pytest.fixture(scope=containers_scope) 172 | def docker_cleanup() -> Union[List[str], str]: 173 | """Get the docker_compose command to be executed for test clean-up actions. 174 | Override this fixture in your tests if you need to change clean-up actions. 175 | Returning anything that would evaluate to False will skip this command.""" 176 | 177 | return get_cleanup_command() 178 | 179 | 180 | def get_setup_command() -> Union[List[str], str]: 181 | return ["up --build -d"] 182 | 183 | 184 | @pytest.fixture(scope=containers_scope) 185 | def docker_setup() -> Union[List[str], str]: 186 | """Get the docker_compose command to be executed for test setup actions. 187 | Override this fixture in your tests if you need to change setup actions. 188 | Returning anything that would evaluate to False will skip this command.""" 189 | 190 | return get_setup_command() 191 | 192 | 193 | @contextlib.contextmanager 194 | def get_docker_services( 195 | docker_compose_command: str, 196 | docker_compose_file: Union[List[str], str], 197 | docker_compose_project_name: str, 198 | docker_setup: Union[List[str], str], 199 | docker_cleanup: Union[List[str], str], 200 | ) -> Iterator[Services]: 201 | docker_compose = DockerComposeExecutor( 202 | docker_compose_command, docker_compose_file, docker_compose_project_name 203 | ) 204 | 205 | # setup containers. 206 | if docker_setup: 207 | # Maintain backwards compatibility with the string format. 208 | if isinstance(docker_setup, str): 209 | docker_setup = [docker_setup] 210 | for command in docker_setup: 211 | docker_compose.execute(command) 212 | 213 | try: 214 | # Let test(s) run. 215 | yield Services(docker_compose) 216 | finally: 217 | # Clean up. 218 | if docker_cleanup: 219 | # Maintain backwards compatibility with the string format. 220 | if isinstance(docker_cleanup, str): 221 | docker_cleanup = [docker_cleanup] 222 | for command in docker_cleanup: 223 | docker_compose.execute(command) 224 | 225 | 226 | @pytest.fixture(scope=containers_scope) 227 | def docker_services( 228 | docker_compose_command: str, 229 | docker_compose_file: Union[List[str], str], 230 | docker_compose_project_name: str, 231 | docker_setup: str, 232 | docker_cleanup: str, 233 | ) -> Iterator[Services]: 234 | """Start all services from a docker compose file (`docker-compose up`). 235 | After test are finished, shutdown all services (`docker-compose down`).""" 236 | 237 | with get_docker_services( 238 | docker_compose_command, 239 | docker_compose_file, 240 | docker_compose_project_name, 241 | docker_setup, 242 | docker_cleanup, 243 | ) as docker_service: 244 | yield docker_service 245 | -------------------------------------------------------------------------------- /src/pytest_docker/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avast/pytest-docker/be1e94708d8827f017f3db07a234ba3f576a2163/src/pytest_docker/py.typed -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = ["pytester"] 2 | -------------------------------------------------------------------------------- /tests/containers/hello/Dockerfile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | FROM python:3.6-alpine 4 | # Python won't pick up SIGTERM (the default) and will have to be killed with SIGKILL 5 | # from Docker after a timeout. But it will pick up SIGINT and stop immediately 6 | # as if touched by Ctrl+c. 7 | STOPSIGNAL SIGINT 8 | 9 | WORKDIR /app 10 | ADD server.py /app 11 | 12 | CMD [ "python", "/app/server.py" ] 13 | -------------------------------------------------------------------------------- /tests/containers/hello/server.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from wsgiref.simple_server import make_server 3 | 4 | 5 | def test_app(_, start_response): # type: ignore 6 | # This path is set up as a volume in the test's docker-compose.yml, 7 | # so we make sure that we really work with Docker Compose. 8 | if path.exists("/test_volume"): 9 | status = "204 No Content" 10 | else: 11 | status = "500 Internal Server Error" 12 | start_response(status, headers=[]) 13 | # and empty HTTP body matching the 204 14 | return [b""] 15 | 16 | 17 | HTTPD = make_server("", 80, test_app) 18 | print("Test server running...") 19 | HTTPD.serve_forever() 20 | -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2' 3 | services: 4 | hello: 5 | build: "./containers/hello" 6 | ports: 7 | - "80" 8 | volumes: 9 | - test_volume:/test_volume 10 | 11 | volumes: 12 | test_volume: 13 | -------------------------------------------------------------------------------- /tests/test_docker_ip.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from unittest import mock 3 | 4 | import pytest 5 | from pytest_docker.plugin import get_docker_ip 6 | 7 | 8 | def test_docker_ip_native() -> None: 9 | environ: Dict[str, str] = {} 10 | with mock.patch("os.environ", environ): 11 | assert get_docker_ip() == "127.0.0.1" 12 | 13 | 14 | def test_docker_ip_remote() -> None: 15 | environ = {"DOCKER_HOST": "tcp://1.2.3.4:2376"} 16 | with mock.patch("os.environ", environ): 17 | assert get_docker_ip() == "1.2.3.4" 18 | 19 | 20 | def test_docker_ip_unix() -> None: 21 | environ = {"DOCKER_HOST": "unix:///run/user/1000/podman/podman.sock"} 22 | with mock.patch("os.environ", environ): 23 | assert get_docker_ip() == "127.0.0.1" 24 | 25 | 26 | @pytest.mark.parametrize("docker_host", ["http://1.2.3.4:2376", "tcp://1.2.3.4:2376"]) 27 | def test_docker_ip_remote_invalid(docker_host: str) -> None: 28 | environ = {"DOCKER_HOST": docker_host} 29 | with mock.patch("os.environ", environ): 30 | assert get_docker_ip() == "1.2.3.4" 31 | -------------------------------------------------------------------------------- /tests/test_docker_services.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from unittest import mock 3 | 4 | import pytest 5 | from pytest_docker.plugin import ( 6 | DockerComposeExecutor, 7 | Services, 8 | get_cleanup_command, 9 | get_docker_services, 10 | get_setup_command, 11 | ) 12 | 13 | 14 | def test_docker_services() -> None: 15 | """Automatic teardown of all services.""" 16 | 17 | with mock.patch("subprocess.check_output") as check_output: 18 | check_output.side_effect = [b"", b"0.0.0.0:32770", b""] 19 | check_output.returncode = 0 20 | 21 | assert check_output.call_count == 0 22 | 23 | # The fixture is a context-manager. 24 | with get_docker_services( 25 | "docker compose", 26 | "docker-compose.yml", 27 | docker_compose_project_name="pytest123", 28 | docker_setup=get_setup_command(), 29 | docker_cleanup=get_cleanup_command(), 30 | ) as services: 31 | assert isinstance(services, Services) 32 | 33 | assert check_output.call_count == 1 34 | 35 | # Can request port for services. 36 | port = services.port_for("abc", 123) 37 | assert port == 32770 38 | 39 | assert check_output.call_count == 2 40 | 41 | # 2nd request for same service should hit the cache. 42 | port = services.port_for("abc", 123) 43 | assert port == 32770 44 | 45 | assert check_output.call_count == 2 46 | 47 | assert check_output.call_count == 3 48 | 49 | # Both should have been called. 50 | assert check_output.call_args_list == [ 51 | mock.call( 52 | 'docker compose -f "docker-compose.yml" -p "pytest123" up --build -d', 53 | stderr=subprocess.STDOUT, 54 | shell=True, 55 | ), 56 | mock.call( 57 | 'docker compose -f "docker-compose.yml" -p "pytest123" port abc 123', 58 | stderr=subprocess.STDOUT, 59 | shell=True, 60 | ), 61 | mock.call( 62 | 'docker compose -f "docker-compose.yml" -p "pytest123" down -v', 63 | stderr=subprocess.STDOUT, 64 | shell=True, 65 | ), 66 | ] 67 | 68 | 69 | def test_docker_services_unused_port() -> None: 70 | """Complain loudly when the requested port is not used by the service.""" 71 | 72 | with mock.patch("subprocess.check_output") as check_output: 73 | check_output.side_effect = [b"", b"", b""] 74 | check_output.returncode = 0 75 | 76 | assert check_output.call_count == 0 77 | 78 | # The fixture is a context-manager. 79 | with get_docker_services( 80 | "docker compose", 81 | "docker-compose.yml", 82 | docker_compose_project_name="pytest123", 83 | docker_setup=get_setup_command(), 84 | docker_cleanup=get_cleanup_command(), 85 | ) as services: 86 | assert isinstance(services, Services) 87 | 88 | assert check_output.call_count == 1 89 | 90 | # Can request port for services. 91 | with pytest.raises(ValueError) as exc: 92 | print(services.port_for("abc", 123)) 93 | assert str(exc.value) == ('Could not detect port for "%s:%d".' % ("abc", 123)) 94 | 95 | assert check_output.call_count == 2 96 | 97 | assert check_output.call_count == 3 98 | 99 | # Both should have been called. 100 | assert check_output.call_args_list == [ 101 | mock.call( 102 | 'docker compose -f "docker-compose.yml" -p "pytest123" ' 103 | "up --build -d", # pylint: disable:=implicit-str-concat 104 | shell=True, 105 | stderr=subprocess.STDOUT, 106 | ), 107 | mock.call( 108 | 'docker compose -f "docker-compose.yml" -p "pytest123" ' 109 | "port abc 123", # pylint: disable:=implicit-str-concat 110 | shell=True, 111 | stderr=subprocess.STDOUT, 112 | ), 113 | mock.call( 114 | 'docker compose -f "docker-compose.yml" -p "pytest123" down -v', # pylint: disable:=implicit-str-concat 115 | shell=True, 116 | stderr=subprocess.STDOUT, 117 | ), 118 | ] 119 | 120 | 121 | def test_docker_services_failure() -> None: 122 | """Propagate failure to start service.""" 123 | 124 | with mock.patch("subprocess.check_output") as check_output: 125 | check_output.side_effect = [subprocess.CalledProcessError(1, "the command", b"the output")] 126 | check_output.returncode = 1 127 | 128 | # The fixture is a context-manager. 129 | with pytest.raises(Exception) as exc: 130 | with get_docker_services( 131 | "docker compose", 132 | "docker-compose.yml", 133 | docker_compose_project_name="pytest123", 134 | docker_setup=get_setup_command(), 135 | docker_cleanup=get_cleanup_command(), 136 | ): 137 | pass 138 | 139 | # Failure propagates with improved diagnoatics. 140 | assert str(exc.value) == ( 141 | 'Command {} returned {}: """{}""".'.format("the command", 1, "the output") 142 | ) 143 | 144 | assert check_output.call_count == 1 145 | 146 | # Tear down code should not be called. 147 | assert check_output.call_args_list == [ 148 | mock.call( 149 | 'docker compose -f "docker-compose.yml" -p "pytest123" ' 150 | "up --build -d", # pylint: disable:=implicit-str-concat 151 | shell=True, 152 | stderr=subprocess.STDOUT, 153 | ) 154 | ] 155 | 156 | 157 | def test_wait_until_responsive_timeout() -> None: 158 | clock = mock.MagicMock() 159 | clock.side_effect = [0.0, 1.0, 2.0, 3.0] 160 | 161 | with mock.patch("time.sleep") as sleep: 162 | docker_compose = DockerComposeExecutor( 163 | compose_command="docker compose", 164 | compose_files="docker-compose.yml", 165 | compose_project_name="pytest123", 166 | ) 167 | services = Services(docker_compose) 168 | with pytest.raises(Exception) as exc: 169 | print( 170 | services.wait_until_responsive( # type: ignore 171 | check=lambda: False, timeout=3.0, pause=1.0, clock=clock 172 | ) 173 | ) 174 | assert sleep.call_args_list == [mock.call(1.0), mock.call(1.0), mock.call(1.0)] 175 | assert str(exc.value) == ("Timeout reached while waiting on service!") 176 | 177 | 178 | def test_single_commands() -> None: 179 | """Ensures backwards compatibility with single command strings for setup and cleanup.""" 180 | 181 | with mock.patch("subprocess.check_output") as check_output: 182 | check_output.returncode = 0 183 | 184 | assert check_output.call_count == 0 185 | 186 | # The fixture is a context-manager. 187 | with get_docker_services( 188 | "docker compose", 189 | "docker-compose.yml", 190 | docker_compose_project_name="pytest123", 191 | docker_setup="up --build -d", 192 | docker_cleanup="down -v", 193 | ) as services: 194 | assert isinstance(services, Services) 195 | 196 | assert check_output.call_count == 1 197 | 198 | # Can request port for services. 199 | port = services.port_for("hello", 80) 200 | assert port == 1 201 | 202 | assert check_output.call_count == 2 203 | 204 | # 2nd request for same service should hit the cache. 205 | port = services.port_for("hello", 80) 206 | assert port == 1 207 | 208 | assert check_output.call_count == 2 209 | 210 | assert check_output.call_count == 3 211 | 212 | # Both should have been called. 213 | assert check_output.call_args_list == [ 214 | mock.call( 215 | 'docker compose -f "docker-compose.yml" -p "pytest123" up --build -d', 216 | stderr=subprocess.STDOUT, 217 | shell=True, 218 | ), 219 | mock.call( 220 | 'docker compose -f "docker-compose.yml" -p "pytest123" port hello 80', 221 | stderr=subprocess.STDOUT, 222 | shell=True, 223 | ), 224 | mock.call( 225 | 'docker compose -f "docker-compose.yml" -p "pytest123" down -v', 226 | stderr=subprocess.STDOUT, 227 | shell=True, 228 | ), 229 | ] 230 | 231 | 232 | def test_multiple_commands() -> None: 233 | """Multiple startup and cleanup commands should be executed.""" 234 | 235 | with mock.patch("subprocess.check_output") as check_output: 236 | check_output.returncode = 0 237 | 238 | assert check_output.call_count == 0 239 | 240 | # The fixture is a context-manager. 241 | with get_docker_services( 242 | "docker compose", 243 | "docker-compose.yml", 244 | docker_compose_project_name="pytest123", 245 | docker_setup=["ps", "up --build -d"], 246 | docker_cleanup=["down -v", "ps"], 247 | ) as services: 248 | assert isinstance(services, Services) 249 | 250 | assert check_output.call_count == 2 251 | 252 | # Can request port for services. 253 | port = services.port_for("hello", 80) 254 | assert port == 1 255 | 256 | assert check_output.call_count == 3 257 | 258 | # 2nd request for same service should hit the cache. 259 | port = services.port_for("hello", 80) 260 | assert port == 1 261 | 262 | assert check_output.call_count == 3 263 | 264 | assert check_output.call_count == 5 265 | 266 | # Both should have been called. 267 | assert check_output.call_args_list == [ 268 | mock.call( 269 | 'docker compose -f "docker-compose.yml" -p "pytest123" ps', 270 | stderr=subprocess.STDOUT, 271 | shell=True, 272 | ), 273 | mock.call( 274 | 'docker compose -f "docker-compose.yml" -p "pytest123" up --build -d', 275 | stderr=subprocess.STDOUT, 276 | shell=True, 277 | ), 278 | mock.call( 279 | 'docker compose -f "docker-compose.yml" -p "pytest123" port hello 80', 280 | stderr=subprocess.STDOUT, 281 | shell=True, 282 | ), 283 | mock.call( 284 | 'docker compose -f "docker-compose.yml" -p "pytest123" down -v', 285 | stderr=subprocess.STDOUT, 286 | shell=True, 287 | ), 288 | mock.call( 289 | 'docker compose -f "docker-compose.yml" -p "pytest123" ps', 290 | stderr=subprocess.STDOUT, 291 | shell=True, 292 | ), 293 | ] 294 | -------------------------------------------------------------------------------- /tests/test_dockercomposeexecutor.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from unittest import mock 3 | 4 | import py 5 | from pytest_docker.plugin import DockerComposeExecutor 6 | 7 | 8 | def test_execute() -> None: 9 | docker_compose = DockerComposeExecutor("docker compose", "docker-compose.yml", "pytest123") 10 | with mock.patch("subprocess.check_output") as check_output: 11 | docker_compose.execute("up") 12 | assert check_output.call_args_list == [ 13 | mock.call( 14 | 'docker compose -f "docker-compose.yml" -p "pytest123" up', 15 | shell=True, 16 | stderr=subprocess.STDOUT, 17 | ) 18 | ] 19 | 20 | 21 | def test_execute_docker_compose_v2() -> None: 22 | docker_compose = DockerComposeExecutor("docker compose", "docker-compose.yml", "pytest123") 23 | with mock.patch("subprocess.check_output") as check_output: 24 | docker_compose.execute("up") 25 | assert check_output.call_args_list == [ 26 | mock.call( 27 | 'docker compose -f "docker-compose.yml" -p "pytest123" up', 28 | shell=True, 29 | stderr=subprocess.STDOUT, 30 | ) 31 | ] 32 | 33 | 34 | def test_pypath_compose_files() -> None: 35 | compose_file: py.path.local = py.path.local("/tmp/docker-compose.yml") 36 | docker_compose = DockerComposeExecutor("docker compose", compose_file, "pytest123") # type: ignore 37 | with mock.patch("subprocess.check_output") as check_output: 38 | docker_compose.execute("up") 39 | assert check_output.call_args_list == [ 40 | mock.call( 41 | 'docker compose -f "/tmp/docker-compose.yml"' 42 | ' -p "pytest123" up', # pylint: disable:=implicit-str-concat 43 | shell=True, 44 | stderr=subprocess.STDOUT, 45 | ) 46 | ] or check_output.call_args_list == [ 47 | mock.call( 48 | 'docker compose -f "C:\\tmp\\docker-compose.yml"' 49 | ' -p "pytest123" up', # pylint: disable:=implicit-str-concat 50 | shell=True, 51 | stderr=subprocess.STDOUT, 52 | ) 53 | ] 54 | 55 | 56 | def test_multiple_compose_files() -> None: 57 | docker_compose = DockerComposeExecutor( 58 | "docker compose", ["docker-compose.yml", "other-compose.yml"], "pytest123" 59 | ) 60 | with mock.patch("subprocess.check_output") as check_output: 61 | docker_compose.execute("up") 62 | assert check_output.call_args_list == [ 63 | mock.call( 64 | 'docker compose -f "docker-compose.yml" -f "other-compose.yml"' 65 | ' -p "pytest123" up', 66 | shell=True, 67 | stderr=subprocess.STDOUT, 68 | ) 69 | ] 70 | -------------------------------------------------------------------------------- /tests/test_fixtures.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from typing import List 3 | 4 | import pytest 5 | from _pytest.fixtures import FixtureRequest 6 | from _pytest.pytester import Pytester 7 | 8 | HERE = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_docker_compose_file(docker_compose_file: str) -> None: 12 | assert docker_compose_file == os.path.join(HERE, "docker-compose.yml") 13 | 14 | 15 | def test_docker_compose_project(docker_compose_project_name: str) -> None: 16 | assert docker_compose_project_name == "pytest{}".format(os.getpid()) 17 | 18 | 19 | def test_docker_cleanup(docker_cleanup: List[str]) -> None: 20 | assert docker_cleanup == ["down -v"] 21 | 22 | 23 | def test_docker_setup(docker_setup: List[str]) -> None: 24 | assert docker_setup == ["up --build -d"] 25 | 26 | 27 | def test_docker_compose_comand(docker_compose_command: str) -> None: 28 | assert docker_compose_command == "docker compose" 29 | 30 | 31 | def test_default_container_scope(pytester: Pytester) -> None: 32 | pytester.makepyfile( 33 | """ 34 | import pytest 35 | @pytest.fixture(scope="session") 36 | def dummy(docker_cleanup): 37 | return True 38 | 39 | def test_default_container_scope(dummy): 40 | assert dummy == True 41 | """ 42 | ) 43 | 44 | result = pytester.runpytest() 45 | result.assert_outcomes(passed=1) 46 | 47 | 48 | @pytest.mark.parametrize("scope", ["session", "module", "class"]) 49 | def test_general_container_scope(testdir: Pytester, request: FixtureRequest, scope: str) -> None: 50 | params = [f"--container-scope={scope}"] 51 | assert request.config.pluginmanager.hasplugin("docker") 52 | 53 | testdir.makepyfile( 54 | f""" 55 | import pytest 56 | @pytest.fixture(scope="{scope}") 57 | def dummy(docker_cleanup): 58 | return True 59 | 60 | def test_default_container_scope(dummy): 61 | assert dummy == True 62 | """ 63 | ) 64 | 65 | result = testdir.runpytest(*params) 66 | result.assert_outcomes(passed=1) 67 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | from os import path 4 | from pathlib import Path 5 | 6 | import requests 7 | from pytest import Testdir 8 | from pytest_docker.plugin import Services 9 | from requests.exceptions import ConnectionError 10 | 11 | 12 | def is_responsive(url: str) -> bool: 13 | """Check if something responds to ``url``.""" 14 | try: 15 | response = requests.get(url) 16 | if response.status_code == 204: 17 | return True 18 | return False 19 | except ConnectionError: 20 | return False 21 | 22 | 23 | def test_main_fixtures_work(docker_ip: str, docker_services: Services) -> None: 24 | """Showcase the power of our Docker fixtures!""" 25 | 26 | # Build URL to service listening on random port. 27 | url = "http://%s:%d/" % (docker_ip, docker_services.port_for("hello", 80)) 28 | 29 | docker_services.wait_until_responsive( 30 | check=lambda: is_responsive(url), timeout=30.0, pause=0.1 31 | ) 32 | 33 | # Contact the service. 34 | response = requests.get(url) 35 | # this is set up in the test image 36 | assert response.status_code == 204 37 | 38 | 39 | def test_containers_and_volumes_get_cleaned_up( 40 | testdir: Testdir, tmpdir: Path, docker_compose_file: Path 41 | ) -> None: 42 | _copy_compose_files_to_testdir(testdir, docker_compose_file) 43 | 44 | project_name_file_path = str(path.join(str(tmpdir), "project_name.txt")).replace("\\", "/") 45 | testdir.makepyfile( 46 | f""" 47 | import subprocess 48 | 49 | def _check_volume_exists(project_name): 50 | check_proc = subprocess.Popen( 51 | "docker volume ls".split(), 52 | stdout=subprocess.PIPE, 53 | ) 54 | assert project_name.encode() in check_proc.stdout.read() 55 | 56 | def _check_container_exists(project_name): 57 | check_proc = subprocess.Popen( 58 | "docker ps".split(), 59 | stdout=subprocess.PIPE, 60 | ) 61 | assert project_name.encode() in check_proc.stdout.read() 62 | 63 | def test_whatever(docker_services, docker_compose_project_name): 64 | _check_volume_exists(docker_compose_project_name) 65 | _check_container_exists(docker_compose_project_name) 66 | with open(f"{project_name_file_path}", 'w') as project_name_file: 67 | project_name_file.write(docker_compose_project_name) 68 | """ 69 | ) 70 | 71 | result = testdir.runpytest() 72 | result.assert_outcomes(passed=1) 73 | 74 | with open(str(project_name_file_path), "rb") as project_name_file: 75 | compose_project_name = project_name_file.read().decode() 76 | _check_volume_is_gone(compose_project_name) 77 | _check_container_is_gone(compose_project_name) 78 | 79 | 80 | def _copy_compose_files_to_testdir(testdir: Testdir, compose_file_path: Path) -> None: 81 | directory_for_compose_files = testdir.mkdir("tests") 82 | shutil.copy(compose_file_path, str(directory_for_compose_files)) 83 | 84 | container_build_files_dir = path.realpath(path.join(compose_file_path, "../containers")) 85 | shutil.copytree(container_build_files_dir, str(directory_for_compose_files) + "/containers") 86 | 87 | 88 | def _check_volume_is_gone(project_name: str) -> None: 89 | check_proc = subprocess.Popen("docker volume ls".split(), stdout=subprocess.PIPE) 90 | assert check_proc.stdout is not None 91 | assert project_name.encode() not in check_proc.stdout.read() 92 | 93 | 94 | def _check_container_is_gone(project_name: str) -> None: 95 | check_proc = subprocess.Popen("docker ps".split(), stdout=subprocess.PIPE) 96 | assert check_proc.stdout is not None 97 | assert project_name.encode() not in check_proc.stdout.read() 98 | --------------------------------------------------------------------------------