Skip to content

Commit 140c2c7

Browse files
authored
Merge pull request pytest-docker-compose#24 from RmStorm/develop
Change how fixtures supply containers to solve dynamic ports issue Closes pytest-docker-compose#8
2 parents 1c0f9a5 + d734fec commit 140c2c7

File tree

9 files changed

+65
-46
lines changed

9 files changed

+65
-46
lines changed

.pycodestyle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pycodestyle]
2+
max-line-length = 119

README.rst

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
pytest-docker-compose
22
=====================
3+
.. image:: https://circleci.com/gh/pytest-docker-compose/pytest-docker-compose/tree/master.svg?style=svg
4+
:target: https://circleci.com/gh/pytest-docker-compose/pytest-docker-compose/tree/master
5+
36
This package contains a `pytest`_ plugin for integrating Docker Compose into your automated integration tests.
47

58
Given a path to a ``docker-compose.yml`` file, it will automatically build the project at the start of the test run, bring the containers up before each test starts, and tear them down after each test ends.
@@ -37,11 +40,8 @@ See `Installing and Using Plugins`_ for more information.
3740

3841
To interact with Docker containers in your tests, use the following fixtures:
3942

40-
``function_scoped_containers``
41-
A dictionary of the Docker ``compose.container.Container`` objects
42-
running during the test. These containers each have an extra attribute
43-
called ``network_info`` added to them. This attribute has a list of
44-
``pytest_docker_compose.NetworkInfo`` objects.
43+
``function_scoped_container_getter``
44+
An object that fetches containers of the Docker ``compose.container.Container`` objects running during the test. The containers are fetched using ``function_scoped_container_getter.get('service_name')`` These containers each have an extra attribute called ``network_info`` added to them. This attribute has a list of ``pytest_docker_compose.NetworkInfo`` objects.
4545

4646
This information can be used to configure API clients and other objects that
4747
will connect to services exposed by the Docker containers in your tests.
@@ -64,18 +64,18 @@ To interact with Docker containers in your tests, use the following fixtures:
6464

6565
To use the following fixtures please read `Use wider scoped fixtures`_.
6666

67-
``class_scoped_containers``
68-
Similar to ``function_scoped_containers`` just with a wider scope.
67+
``class_scoped_container_getter``
68+
Similar to ``function_scoped_container_getter`` just with a wider scope.
6969

70-
``module_scoped_containers``
71-
Similar to ``function_scoped_containers`` just with a wider scope.
70+
``module_scoped_container_getter``
71+
Similar to ``function_scoped_container_getter`` just with a wider scope.
7272

73-
``session_scoped_containers``
74-
Similar to ``function_scoped_containers`` just with a wider scope.
73+
``session_scoped_container_getter``
74+
Similar to ``function_scoped_container_getter`` just with a wider scope.
7575

7676
Waiting for Services to Come Online
7777
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78-
The fixtures called ``'scope'_scoped_containers`` will wait until every container is up before handing control over to the test.
78+
The fixtures called ``'scope'_scoped_container_getter`` will wait until every container is up before handing control over to the test.
7979

8080
However, just because a container is up does not mean that the services running on it are ready to accept incoming requests yet!
8181

@@ -95,15 +95,15 @@ Here's an example of a fixture called ``wait_for_api`` that waits for an HTTP se
9595
9696
9797
@pytest.fixture(scope="function")
98-
def wait_for_api(function_scoped_containers):
98+
def wait_for_api(function_scoped_container_getter):
9999
"""Wait for the api from my_api_service to become responsive"""
100100
request_session = requests.Session()
101101
retries = Retry(total=5,
102102
backoff_factor=0.1,
103103
status_forcelist=[500, 502, 503, 504])
104104
request_session.mount('http://', HTTPAdapter(max_retries=retries))
105105
106-
service = function_scoped_containers["my_network_my_api_service_1"].network_info[0]
106+
service = function_scoped_container_getter.get("my_api_service").network_info[0]
107107
api_url = "http://%s:%s/" % (service.hostname, service.host_port)
108108
assert request_session.get(api_url)
109109
return request_session, api_url
@@ -120,11 +120,11 @@ Here's an example of a fixture called ``wait_for_api`` that waits for an HTTP se
120120
121121
Use wider scoped fixtures
122122
~~~~~~~~~~~~~~~~~~~~~~~~~
123-
The ``function_scoped_containers`` fixture uses "function" scope, meaning that all of the containers are torn down after each individual test.
123+
The ``function_scoped_container_getter`` fixture uses "function" scope, meaning that all of the containers are torn down after each individual test.
124124

125125
This is done so that every test gets to run in a "clean" environment. However, this can potentially make a test suite take a very long time to complete.
126126

127-
There are two options to make containers persist beyond a single test. The best way is to use the fixtures that are explicitly scoped to different scopes. There are three additional fixtures for this purpose: ``class_scoped_containers``, ``module_scoped_containers`` and ``session_scoped_containers``. Notice that you need to be careful when using these! There are two main caveats to keep in mind:
127+
There are two options to make containers persist beyond a single test. The best way is to use the fixtures that are explicitly scoped to different scopes. There are three additional fixtures for this purpose: ``class_scoped_container_getter``, ``module_scoped_container_getter`` and ``session_scoped_container_getter``. Notice that you need to be careful when using these! There are two main caveats to keep in mind:
128128

129129
1. Manage your scope correctly, using 'module' scope and 'function' scope in one single file will throw an error! This is because the module scoped fixture will spin up the containers and then the function scoped fixture will try to spin up the containers again. Docker compose does not allow you to spin up containers twice.
130130
2. Clean up your environment after each test. Because the containers are not restarted their environments can carry the information from previous tests. Therefore you need to be very carefull when designing your tests such that they leave the containers in the same state that it started in or you might run into difficult to understand behaviour.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
version="2.0.0",
1111
author="Roald Storm",
1212
author_email="[email protected]",
13-
url="https://github.com/RmStorm/pytest-docker-compose",
13+
url="https://github.com/pytest-docker-compose/pytest-docker-compose",
1414
packages=find_packages(where="src"),
1515
package_dir={"": "src"},
1616
install_requires=["docker-compose", "pytest >= 3.4"],

src/pytest_docker_compose/__init__.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import typing
1+
from typing import List
22
from pathlib import Path
33
import warnings
44
from datetime import datetime
@@ -23,8 +23,7 @@ class ContainersAlreadyExist(Exception):
2323

2424

2525
class NetworkInfo:
26-
def __init__(self, container_port: typing.Text,
27-
hostname: typing.Text, host_port: int,):
26+
def __init__(self, container_port: str, hostname: str, host_port: int,):
2827
"""
2928
Container for info about how to connect to a service exposed by a
3029
Docker container.
@@ -39,7 +38,7 @@ def __init__(self, container_port: typing.Text,
3938
self.host_port = host_port
4039

4140

42-
def create_network_info_for_container(container):
41+
def create_network_info_for_container(container: Container):
4342
"""
4443
Generates :py:class:`NetworkInfo` instances corresponding to all available
4544
port bindings in a container
@@ -48,7 +47,7 @@ def create_network_info_for_container(container):
4847
hostname=port_config["HostIp"] or "localhost",
4948
host_port=port_config["HostPort"],)
5049
for container_port, port_configs in
51-
container.get("HostConfig.PortBindings").items()
50+
container.ports.items()
5251
for port_config in port_configs]
5352

5453

@@ -57,10 +56,10 @@ class DockerComposePlugin:
5756
Integrates docker-compose into pytest integration tests.
5857
"""
5958
def __init__(self):
60-
self.function_scoped_containers = self.generate_scoped_containers_fixture('function')
61-
self.class_scoped_containers = self.generate_scoped_containers_fixture('class')
62-
self.module_scoped_containers = self.generate_scoped_containers_fixture('module')
63-
self.session_scoped_containers = self.generate_scoped_containers_fixture('session')
59+
self.function_scoped_container_getter = self.generate_scoped_containers_fixture('function')
60+
self.class_scoped_container_getter = self.generate_scoped_containers_fixture('class')
61+
self.module_scoped_container_getter = self.generate_scoped_containers_fixture('module')
62+
self.session_scoped_container_getter = self.generate_scoped_containers_fixture('session')
6463

6564
# noinspection SpellCheckingInspection
6665
@staticmethod
@@ -140,7 +139,7 @@ def docker_project(self, request):
140139
return project
141140

142141
@classmethod
143-
def generate_scoped_containers_fixture(cls, scope):
142+
def generate_scoped_containers_fixture(cls, scope: str):
144143
"""
145144
Create scoped fixtures that retrieve or spin up all containers, and add
146145
network info objects to containers and then yield the containers for
@@ -153,20 +152,19 @@ def generate_scoped_containers_fixture(cls, scope):
153152
def scoped_containers_fixture(docker_project: Project, request):
154153
now = datetime.utcnow()
155154
if request.config.getoption("--use-running-containers"):
156-
containers = docker_project.containers() # type: typing.List[Container]
155+
containers = docker_project.containers() # type: List[Container]
157156
else:
158157
if any(docker_project.containers()):
159158
raise ContainersAlreadyExist(
160159
'pytest-docker-compose tried to start containers but there are'
161160
' already running containers: %s, you probably scoped your'
162161
' tests wrong' % docker_project.containers())
163-
containers = docker_project.up() # type: typing.List[Container]
162+
containers = docker_project.up()
164163
if not containers:
165164
raise ValueError("`docker-compose` didn't launch any containers!")
166165

167-
for container in containers:
168-
container.network_info = create_network_info_for_container(container)
169-
yield {container.name: container for container in containers}
166+
container_getter = ContainerGetter(docker_project)
167+
yield container_getter
170168

171169
for container in sorted(containers, key=lambda c: c.name):
172170
header = "Logs from {name}:".format(name=container.name)
@@ -177,13 +175,27 @@ def scoped_containers_fixture(docker_project: Project, request):
177175
if not request.config.getoption("--use-running-containers"):
178176
docker_project.down(ImageType.none, False)
179177
scoped_containers_fixture.__wrapped__.__doc__ = """
180-
Spins up the containers for the Docker project and returns them in a
181-
dictionary. Each container has one additional attribute called
182-
network_info to simplify accessing the hostnames and exposed port
183-
numbers for each container.
178+
Spins up the containers for the Docker project and returns an
179+
object that can retrieve the containers. The returned containers
180+
all have one additional attribute called network_info to simplify
181+
accessing the hostnames and exposed port numbers for each container.
184182
This set of containers is scoped to '%s'
185183
""" % scope
186184
return scoped_containers_fixture
187185

188186

189187
plugin = DockerComposePlugin()
188+
189+
190+
class ContainerGetter:
191+
"""
192+
A class that retrieves containers from the docker project and adds a
193+
convenience wrapper for the available ports
194+
"""
195+
def __init__(self, docker_project: Project) -> None:
196+
self.docker_project = docker_project
197+
198+
def get(self, key: str) -> Container:
199+
container = self.docker_project.containers(service_names=[key])[0]
200+
container.network_info = create_network_info_for_container(container)
201+
return container

tests/pytest_docker_compose_tests/my_network/docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ services:
44
my_api_service:
55
build: ./a_buildable_container
66
ports:
7-
- 5000:5000
7+
- "5000:5000"
88
depends_on:
99
- my_db
1010
restart: on-failure
1111

1212
my_db:
1313
image: postgres:11.2-alpine
1414
ports:
15-
- 5432:5432
15+
- "5432:5432"

tests/pytest_docker_compose_tests/test_function_scoping_fixtures.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88

99

1010
@pytest.fixture(scope="function")
11-
def wait_for_api(function_scoped_containers):
11+
def wait_for_api(function_scoped_container_getter):
1212
"""Wait for the api from my_api_service to become responsive"""
1313
request_session = requests.Session()
1414
retries = Retry(total=5,
1515
backoff_factor=0.1,
1616
status_forcelist=[500, 502, 503, 504])
1717
request_session.mount('http://', HTTPAdapter(max_retries=retries))
1818

19-
service = function_scoped_containers["my_network_my_api_service_1"].network_info[0]
19+
service = function_scoped_container_getter.get("my_api_service").network_info[0]
2020
api_url = "http://%s:%s/" % (service.hostname, service.host_port)
2121
assert request_session.get(api_url)
2222
return request_session, api_url

tests/pytest_docker_compose_tests/test_module_scoping_fixtures.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88

99

1010
@pytest.fixture(scope="module")
11-
def wait_for_api(module_scoped_containers):
11+
def wait_for_api(module_scoped_container_getter):
1212
"""Wait for the api from my_api_service to become responsive"""
1313
request_session = requests.Session()
1414
retries = Retry(total=5,
1515
backoff_factor=0.1,
1616
status_forcelist=[500, 502, 503, 504])
1717
request_session.mount('http://', HTTPAdapter(max_retries=retries))
1818

19-
service = module_scoped_containers["my_network_my_api_service_1"].network_info[0]
19+
service = module_scoped_container_getter.get("my_api_service").network_info[0]
2020
api_url = "http://%s:%s/" % (service.hostname, service.host_port)
2121
assert request_session.get(api_url)
2222
return request_session, api_url

tests/pytest_docker_compose_tests/test_wrong_scoping.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44

55
@pytest.mark.should_fail
6-
def test_read_all_module(module_scoped_containers):
7-
assert module_scoped_containers["my_network_my_api_service_1"].network_info[0]
6+
def test_read_all_module(module_scoped_container_getter):
7+
assert module_scoped_container_getter.get("my_api_service").network_info[0]
88

99

1010
@pytest.mark.should_fail
11-
def test_read_all_function(function_scoped_containers):
12-
assert function_scoped_containers["my_network_my_api_service_1"].network_info[0]
11+
def test_read_all_function(function_scoped_container_getter):
12+
assert function_scoped_container_getter.get("my_api_service").network_info[0]

tox.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ deps =
66
pytest3: pytest>=3,<4
77
pytest4: pytest>=4,<5
88
pytest5: pytest>=5,<6
9+
docker-compose==1.24.1
10+
pycodestyle
11+
mypy
912
whitelist_externals=
1013
bash
1114
commands=
15+
pycodestyle --config .pycodestyle src
16+
mypy --namespace-packages --ignore-missing-imports src
1217
bash -c '! pytest -m should_fail'
1318
pytest
1419
docker-compose -f tests/pytest_docker_compose_tests/my_network/docker-compose.yml up -d

0 commit comments

Comments
 (0)