Skip to content

Commit 6fb1a68

Browse files
added testcontainer for RabbitMQ (#162)
* added testcontainer for RabbitMQ Added a testcontainer for the message broker RabbitMQ. It includes ready-to-use config params for the `pika` client library. This library was added to the dependencies. * Fix linting errors in RabbitMQ * fix spelling in readme Co-authored-by: Naomi Elstein <[email protected]>
1 parent d48294c commit 6fb1a68

File tree

9 files changed

+184
-27
lines changed

9 files changed

+184
-27
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,7 @@ docs/_build/
6363
.idea/
6464
.venv/
6565
.testrepository/
66+
67+
# vscode:
68+
.devcontainer/
69+
.vscode/

README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Currently available features:
2222
* Microsoft SQL Server container
2323
* Generic docker containers
2424
* LocalStack
25+
* RabbitMQ
2526

2627
Installation
2728
------------
@@ -75,4 +76,4 @@ We recommend you use a `virtual environment <https://virtualenv.pypa.io/en/stabl
7576
Adding requirements
7677
^^^^^^^^^^^^^^^^^^^
7778

78-
We use :code:`pip-tools` to resolve and manage dependencies. If you need to add a dependency to testcontainers or one of the extras, run :code:`pip install pip-tools` followed by :code:`make requirements` to update the requirements files.
79+
We use :code:`pip-tools` to resolve and manage dependencies. If you need to add a dependency to testcontainers or one of the extras, modify the :code:`setup.py` as well as the :code:`requirements.in` accordingly and then run :code:`pip install pip-tools` followed by :code:`make requirements` to update the requirements files.

requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-e file:.[docker-compose,mysql,oracle,postgresql,selenium,google-cloud-pubsub,mongo,redis,mssqlserver,neo4j,kafka]
1+
-e file:.[docker-compose,mysql,oracle,postgresql,selenium,google-cloud-pubsub,mongo,redis,mssqlserver,neo4j,kafka,rabbitmq]
22
codecov>=2.1.0
33
flake8
44
pytest

requirements/3.6.txt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# This file is autogenerated by pip-compile
2+
# This file is autogenerated by pip-compile with python 3.6
33
# To update, run:
44
#
55
# pip-compile --output-file=requirements/3.6.txt requirements.in
@@ -43,13 +43,13 @@ deprecation==2.1.0
4343
# via testcontainers
4444
distro==1.5.0
4545
# via docker-compose
46-
docker-compose==1.26.2
47-
# via testcontainers
4846
docker[ssh]==4.2.2
4947
# via
5048
# -r requirements.in
5149
# docker-compose
5250
# testcontainers
51+
docker-compose==1.26.2
52+
# via testcontainers
5353
dockerpty==0.4.1
5454
# via docker-compose
5555
docopt==0.6.2
@@ -91,7 +91,8 @@ jinja2==2.11.2
9191
# via sphinx
9292
jsonschema==3.2.0
9393
# via docker-compose
94-
kafka-python==2.0.2 # via testcontainers
94+
kafka-python==2.0.2
95+
# via testcontainers
9596
markupsafe==1.1.1
9697
# via jinja2
9798
mccabe==0.6.1
@@ -107,6 +108,8 @@ packaging==20.4
107108
# sphinx
108109
paramiko==2.7.1
109110
# via docker
111+
pika==1.2.0
112+
# via testcontainers
110113
pluggy==0.13.1
111114
# via pytest
112115
protobuf==3.13.0
@@ -117,12 +120,12 @@ psycopg2-binary==2.8.5
117120
# via testcontainers
118121
py==1.9.0
119122
# via pytest
120-
pyasn1-modules==0.2.8
121-
# via google-auth
122123
pyasn1==0.4.8
123124
# via
124125
# pyasn1-modules
125126
# rsa
127+
pyasn1-modules==0.2.8
128+
# via google-auth
126129
pycodestyle==2.6.0
127130
# via flake8
128131
pycparser==2.20
@@ -143,12 +146,12 @@ pyparsing==2.4.7
143146
# via packaging
144147
pyrsistent==0.16.0
145148
# via jsonschema
146-
pytest-cov==2.10.1
147-
# via -r requirements.in
148149
pytest==6.0.1
149150
# via
150151
# -r requirements.in
151152
# pytest-cov
153+
pytest-cov==2.10.1
154+
# via -r requirements.in
152155
python-dotenv==0.14.0
153156
# via docker-compose
154157
pytz==2020.1

requirements/3.7.txt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# This file is autogenerated by pip-compile
2+
# This file is autogenerated by pip-compile with python 3.7
33
# To update, run:
44
#
55
# pip-compile --output-file=requirements/3.7.txt requirements.in
@@ -43,13 +43,13 @@ deprecation==2.1.0
4343
# via testcontainers
4444
distro==1.5.0
4545
# via docker-compose
46-
docker-compose==1.26.2
47-
# via testcontainers
4846
docker[ssh]==4.2.2
4947
# via
5048
# -r requirements.in
5149
# docker-compose
5250
# testcontainers
51+
docker-compose==1.26.2
52+
# via testcontainers
5353
dockerpty==0.4.1
5454
# via docker-compose
5555
docopt==0.6.2
@@ -91,7 +91,8 @@ jinja2==2.11.2
9191
# via sphinx
9292
jsonschema==3.2.0
9393
# via docker-compose
94-
kafka-python==2.0.2 # via testcontainers
94+
kafka-python==2.0.2
95+
# via testcontainers
9596
markupsafe==1.1.1
9697
# via jinja2
9798
mccabe==0.6.1
@@ -107,6 +108,8 @@ packaging==20.4
107108
# sphinx
108109
paramiko==2.7.1
109110
# via docker
111+
pika==1.2.0
112+
# via testcontainers
110113
pluggy==0.13.1
111114
# via pytest
112115
protobuf==3.13.0
@@ -117,12 +120,12 @@ psycopg2-binary==2.8.5
117120
# via testcontainers
118121
py==1.9.0
119122
# via pytest
120-
pyasn1-modules==0.2.8
121-
# via google-auth
122123
pyasn1==0.4.8
123124
# via
124125
# pyasn1-modules
125126
# rsa
127+
pyasn1-modules==0.2.8
128+
# via google-auth
126129
pycodestyle==2.6.0
127130
# via flake8
128131
pycparser==2.20
@@ -143,12 +146,12 @@ pyparsing==2.4.7
143146
# via packaging
144147
pyrsistent==0.16.0
145148
# via jsonschema
146-
pytest-cov==2.10.1
147-
# via -r requirements.in
148149
pytest==6.0.1
149150
# via
150151
# -r requirements.in
151152
# pytest-cov
153+
pytest-cov==2.10.1
154+
# via -r requirements.in
152155
python-dotenv==0.14.0
153156
# via docker-compose
154157
pytz==2020.1

requirements/3.8.txt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# This file is autogenerated by pip-compile
2+
# This file is autogenerated by pip-compile with python 3.8
33
# To update, run:
44
#
55
# pip-compile --output-file=requirements/3.8.txt requirements.in
@@ -43,13 +43,13 @@ deprecation==2.1.0
4343
# via testcontainers
4444
distro==1.5.0
4545
# via docker-compose
46-
docker-compose==1.26.2
47-
# via testcontainers
4846
docker[ssh]==4.2.2
4947
# via
5048
# -r requirements.in
5149
# docker-compose
5250
# testcontainers
51+
docker-compose==1.26.2
52+
# via testcontainers
5353
dockerpty==0.4.1
5454
# via docker-compose
5555
docopt==0.6.2
@@ -85,7 +85,8 @@ jinja2==2.11.2
8585
# via sphinx
8686
jsonschema==3.2.0
8787
# via docker-compose
88-
kafka-python==2.0.2 # via testcontainers
88+
kafka-python==2.0.2
89+
# via testcontainers
8990
markupsafe==1.1.1
9091
# via jinja2
9192
mccabe==0.6.1
@@ -101,6 +102,8 @@ packaging==20.4
101102
# sphinx
102103
paramiko==2.7.1
103104
# via docker
105+
pika==1.2.0
106+
# via testcontainers
104107
pluggy==0.13.1
105108
# via pytest
106109
protobuf==3.13.0
@@ -111,12 +114,12 @@ psycopg2-binary==2.8.5
111114
# via testcontainers
112115
py==1.9.0
113116
# via pytest
114-
pyasn1-modules==0.2.8
115-
# via google-auth
116117
pyasn1==0.4.8
117118
# via
118119
# pyasn1-modules
119120
# rsa
121+
pyasn1-modules==0.2.8
122+
# via google-auth
120123
pycodestyle==2.6.0
121124
# via flake8
122125
pycparser==2.20
@@ -137,12 +140,12 @@ pyparsing==2.4.7
137140
# via packaging
138141
pyrsistent==0.16.0
139142
# via jsonschema
140-
pytest-cov==2.10.1
141-
# via -r requirements.in
142143
pytest==6.0.1
143144
# via
144145
# -r requirements.in
145146
# pytest-cov
147+
pytest-cov==2.10.1
148+
# via -r requirements.in
146149
python-dotenv==0.14.0
147150
# via docker-compose
148151
pytz==2020.1

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
'redis': ['redis'],
6464
'mssqlserver': ['pyodbc'],
6565
'neo4j': ['neo4j'],
66-
'kafka': ['kafka-python']
66+
'kafka': ['kafka-python'],
67+
'rabbitmq': ['pika'],
6768
},
6869
long_description_content_type="text/x-rst",
6970
long_description=long_description,

testcontainers/rabbitmq.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import os
2+
from typing import Optional
3+
4+
import pika
5+
from testcontainers.core.container import DockerContainer
6+
from testcontainers.core.waiting_utils import wait_container_is_ready
7+
8+
9+
class RabbitMqContainer(DockerContainer):
10+
"""
11+
Test container for RabbitMQ.
12+
13+
Example
14+
-------
15+
The example spins up a RabbitMQ broker and uses the `pika` client library
16+
(https://pypi.org/project/pika/) establish a connection to the broker.
17+
::
18+
from testcontainer.rabbitmq import RabbitMqContainer
19+
import pika
20+
21+
with RabbitMqContainer("rabbitmq:3.9.10") as rabbitmq:
22+
23+
connection = pika.BlockingConnection(rabbitmq.get_connection_params())
24+
channel = connection.channel()
25+
"""
26+
27+
RABBITMQ_NODE_PORT = os.environ.get("RABBITMQ_NODE_PORT", 5672)
28+
RABBITMQ_DEFAULT_USER = os.environ.get("RABBITMQ_DEFAULT_USER", "guest")
29+
RABBITMQ_DEFAULT_PASS = os.environ.get("RABBITMQ_DEFAULT_PASS", "guest")
30+
31+
def __init__(
32+
self,
33+
image: str = "rabbitmq:latest",
34+
port: Optional[int] = None,
35+
username: Optional[str] = None,
36+
password: Optional[str] = None,
37+
) -> None:
38+
"""Initialize the RabbitMQ test container.
39+
40+
Args:
41+
image (str, optional):
42+
The docker image from docker hub. Defaults to "rabbitmq:latest".
43+
port (int, optional):
44+
The port to reach the AMQP API. Defaults to 5672.
45+
username (str, optional):
46+
Overwrite the default username which is "guest".
47+
password (str, optional):
48+
Overwrite the default username which is "guest".
49+
"""
50+
super(RabbitMqContainer, self).__init__(image=image)
51+
self.RABBITMQ_NODE_PORT = port or int(self.RABBITMQ_NODE_PORT)
52+
self.RABBITMQ_DEFAULT_USER = username or self.RABBITMQ_DEFAULT_USER
53+
self.RABBITMQ_DEFAULT_PASS = password or self.RABBITMQ_DEFAULT_PASS
54+
55+
self.with_exposed_ports(self.RABBITMQ_NODE_PORT)
56+
self.with_env("RABBITMQ_NODE_PORT", self.RABBITMQ_NODE_PORT)
57+
self.with_env("RABBITMQ_DEFAULT_USER", self.RABBITMQ_DEFAULT_USER)
58+
self.with_env("RABBITMQ_DEFAULT_PASS", self.RABBITMQ_DEFAULT_PASS)
59+
60+
@wait_container_is_ready()
61+
def readiness_probe(self) -> bool:
62+
"""Test if the RabbitMQ broker is ready."""
63+
connection = pika.BlockingConnection(self.get_connection_params())
64+
if connection.is_open:
65+
connection.close()
66+
return self
67+
raise RuntimeError("Could not open connection to RabbitMQ broker.")
68+
69+
def get_connection_params(self) -> pika.ConnectionParameters:
70+
"""
71+
Get connection params as a pika.ConnectionParameters object.
72+
For more details see:
73+
https://pika.readthedocs.io/en/latest/modules/parameters.html
74+
"""
75+
credentials = pika.PlainCredentials(username=self.RABBITMQ_DEFAULT_USER,
76+
password=self.RABBITMQ_DEFAULT_PASS)
77+
78+
return pika.ConnectionParameters(
79+
host=self.get_container_host_ip(),
80+
port=self.get_exposed_port(self.RABBITMQ_NODE_PORT),
81+
credentials=credentials,
82+
)
83+
84+
def start(self):
85+
"""Start the test container."""
86+
super().start()
87+
self.readiness_probe()
88+
return self

tests/test_rabbitmq.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import Optional
2+
import json
3+
4+
import pika
5+
import pytest
6+
from testcontainers.rabbitmq import RabbitMqContainer
7+
8+
QUEUE = "test-q"
9+
EXCHANGE = "test-exchange"
10+
ROUTING_KEY = "test-route-key"
11+
MESSAGE = {"hello": "world"}
12+
13+
14+
@pytest.mark.parametrize(
15+
"port,username,password",
16+
[
17+
(None, None, None), # use the defaults
18+
(5673, None, None), # test with custom port
19+
(None, "my_test_user", "my_secret_password"), # test with custom credentials
20+
]
21+
)
22+
def test_docker_run_rabbitmq(
23+
port: Optional[int],
24+
username: Optional[str],
25+
password: Optional[str]
26+
):
27+
"""Run rabbitmq test container and use it to deliver a simple message."""
28+
kwargs = {}
29+
if port is not None:
30+
kwargs["port"] = port
31+
if username is not None:
32+
kwargs["username"] = username
33+
if password is not None:
34+
kwargs["password"] = password
35+
36+
rabbitmq_container = RabbitMqContainer("rabbitmq:latest", **kwargs)
37+
with rabbitmq_container as rabbitmq:
38+
# connect to rabbitmq:
39+
connection_params = rabbitmq.get_connection_params()
40+
connection = pika.BlockingConnection(connection_params)
41+
42+
# create exchange and queue:
43+
channel = connection.channel()
44+
channel.exchange_declare(exchange=EXCHANGE, exchange_type="topic")
45+
channel.queue_declare(QUEUE, arguments={})
46+
channel.queue_bind(QUEUE, EXCHANGE, ROUTING_KEY)
47+
48+
# pulish message:
49+
encoded_message = json.dumps(MESSAGE)
50+
channel.basic_publish(EXCHANGE, ROUTING_KEY, body=encoded_message)
51+
52+
_, _, body = channel.basic_get(queue=QUEUE)
53+
received_message = json.loads(body.decode())
54+
assert received_message == MESSAGE

0 commit comments

Comments
 (0)