End-to-end test orchestration with Docker.
Requirements:
- Python 2.7.5
- Docker >= 0.10.0
-
Install Galley:
pip install -r requirements python setup.py install
-
Run Galley:
$ galley -h usage: galley [-h] [--no-destroy] [config] [pattern] End-to-end testing orchestration with Docker. positional arguments: config Path to galley YAML file that defines Docker resources. pattern Test file pattern. optional arguments: -h, --help show this help message and exit --no-destroy Do not destroy images and containers.
You define the environment you want Galley to create in the .galley.yml
file. Place this file in your application's root directory. .galley.yml
consists of three sections: images
, resources
, and testparams
.
images:
0:
name: "redis"
source: "dockerfile/redis"
tag: "latest"
action: "pull"
persist: True
1:
name: "mongodb"
source: "dockerfile/mongodb"
tag: "latest"
action: "pull"
persist: True
2:
name: "baconpancakes"
source: "/path/to/baconpancakes"
action: "build"
persist: False
In the images
section, you describe what images you want Galley to create and how you would like to create them.
name
: A name for the image in Galley. This can be used byresources
to describe what image to use when creating them. Forbuild
actions, this is also the name tagged to the image built.source
: For images with anpull
action, this is therepo/image
of the image to be pulled. Forbuild
actions, this is the path to the directory containing theDockerfile
to build. If.galley.yml
is in the same directory as yourDockerfile
, this can be"."
.tag
: The image tag to pull.action
: Available actions arepull
andbuild
:pull
uses thedocker pull
command to pull the image described insource
from the Docker Index.build
uses thedocker build
command to build an image from theDockerfile
located in the directory described insource
.
persist
(optional): When Galley finishes testing, it destroys all images it created. To keep images from one Galley run to the next, setpersist
toTrue
. This is helpful if you have upstream images you will use every time since they will not have to be downloaded on each run.
testparams:
pancakes: "{{environ['GALLEY_PANCAKES']}}"
bacon: "{{environ['GALLEY_BACON']}}"
testparams
are key-value attributes that can be referenced by your tests.
resources:
0:
name: "redis"
image: "{{redis}}"
host_port: "{{random_port}}"
cont_port: 6379
1:
name: "mongodb"
image: "{{mongodb}}"
host_port: "{{random_port}}"
cont_port: 27017
2:
name: "baconpancakes"
image: "{{baconpancakes}}"
host_port: "{{random_port}}"
cont_port: 8080
command: "--role api"
host_volume: "./docker/config"
cont_volume: "config"
environment:
BACONPANCAKES_ADDRESS: "0.0.0.0:8080"
BACONPANCAKES_USERNAME: "{{environ['GALLEY_BACONPANCAKES_USERNAME']}}"
BACONPANCAKES_PASSWORD: "{{environ['GALLEY_BACONPANCAKES_PASSWORD']}}"
BACONPANCAKES_BROKER_URL: "redis://{{host['ip']}}:{{resources[0]['host_port']}}"
BACONPANCAKES_CELERY_BACKEND: "redis://{{host['ip']}}:{{resources[0]['host_port']}}"
BACONPANCAKES_CONNECTION_STRING: "mongodb://{{host['ip']}}:{{resources[1]['host_port']}}"
In the resources
section, you describe what containers you would like Galley to create and run from the images in the images
section.
name
: The name of the resource. Currently this is not used by anything, but in the furture should be used as the name of the container and as a key when referenced by other resources.image
: Theid
of the image to use to create the container. By using{{image_name}}
, Galley will replace this with the actual imageid
when the image is created.host_port
: The port on the host to map tocont_port
. By using{{random_port}}
a random available ephemeral port on the host will be selected.cont_port
: The port in the container to map tohost_port
.command
: The command to use when running the container.host_volume
: A directory on the host to mount into your container ascont_volume
. Note: volumes are currently not supported by any of the Docker options in OS X. This option only works in Linux.cont_volume
: The directory in the container wherehost_volume
will be mounted.environment
: Environment variables to inject into container when run.
{{resources[..]}}
:- In the resources section, you can build a relationship from one resource to another by referencing another resources data. For example, since we are telling Galley to choose a
{{random_port}}
for our MongoDB and Redis instances, ourbaconpancakes
app won't know how to talk to them. So, in the environment section we tellbaconpancakes
to find the Celery backend with thehost_port
fromresource 0
by using{{resources[0]['host_port']}}
in the connection string. This tells Galley to go find the value ofhost_port
forresource 0
and fill it in. - Currently, Galley is not smart enough to resolve dependencies on its own; therefore, a resource can only reference values from resources that appear before it in the
.galley.yml
file. In the future, this will likely be resolved by explicitly describing dependencies. - Only available in the
resources
section.
- In the resources section, you can build a relationship from one resource to another by referencing another resources data. For example, since we are telling Galley to choose a
{{host[..]}}
:- Host-level information can be referenced through the
host
dictionary. The main usage of this is to provide the host's IP address in order to allow separate resources to communicate with each other. - Currently, the only host-level attribute available in
host
isip
. - Only available in the
resources
section.
- Host-level information can be referenced through the
{{environ[..]}}
:- Replaced with referenced host environment variable.
images:
0:
name: "redis"
source: "dockerfile/redis"
tag: "latest"
action: "pull"
persist: True
1:
name: "mongodb"
source: "dockerfile/mongodb"
tag: "latest"
action: "pull"
persist: True
2:
name: "baconpancakes"
source: "/path/to/baconpancakes"
action: "build"
persist: False
resources:
0:
name: "redis"
image: "{{redis}}"
host_port: "{{random_port}}"
cont_port: 6379
1:
name: "mongodb"
image: "{{mongodb}}"
host_port: "{{random_port}}"
cont_port: 27017
2:
name: "baconpancakes"
image: "{{baconpancakes}}"
host_port: "{{random_port}}"
cont_port: 8080
command: "--role api"
host_volume: "./docker/config"
cont_volume: "config"
environment:
BACONPANCAKES_ADDRESS: "0.0.0.0:8080"
BACONPANCAKES_USERNAME: "{{environ['GALLEY_BACONPANCAKES_USERNAME']}}"
BACONPANCAKES_PASSWORD: "{{environ['GALLEY_BACONPANCAKES_PASSWORD']}}"
BACONPANCAKES_BROKER_URL: "redis://{{host['ip']}}:{{resources[0]['host_port']}}"
BACONPANCAKES_CELERY_BACKEND: "redis://{{host['ip']}}:{{resources[0]['host_port']}}"
BACONPANCAKES_CONNECTION_STRING: "mongodb://{{host['ip']}}:{{resources[1]['host_port']}}"
testparams:
pancakes: "{{environ['GALLEY_PANCAKES']}}"
bacon: "{{environ['GALLEY_BACON']}}"
After Galley completes creating your environment, it looks recursively for any galleytest_*.py
files in your current directory.
Writing tests for Galley to use is easy! Galley uses Python unittests to test your environment. Therefore, writing a test for Galley is just as easy and allows you to use any of unittest
's assert methods. All you need to do is make sure to import GalleyTestCase
from galley.test
and pass GalleyTestCase
into your test class:
import requests
from galley.test import GalleyTestCase
class TestWebGetRequest(GalleyTestCase):
def test_web_status(self):
env = self.environment
web_ip = env['host']['ip']
web_port = env['resources'][2]['host_port']
url = "http://%s:%d" % (web_ip, web_port)
response = requests.get(url)
self.assertEqual(200, response.status_code)
self.assertIn('<title>MakinBaconPancakes</title>', response.text)
In this test we want to check to make sure our web application started properly and that the some expected content was found on the page. Since we imported GalleyTestCase
and passed it into our test class, we can also reference our entire environment in our test by calling self.envionment
. Here, we used this to find the IP address of the Docker host and the port our web application was mapped to. As you can see, the self.assertEqual()
and self.assertIn()
functions come straight from unittests
.
Galley tests can be more complicated as well:
import requests
import time
from galley.test import GalleyTestCase
class TestPancakes(GalleyTestCase):
def test_pancakes(self):
env = self.environment
api_ip = env['host']['ip']
api_port = env['resources'][2]['host_port']
pancakes = env['testparams']['pancakes']
bacon = env['testparams']['bacon']
url = "http://%s:%d/api/%s/%s" % (api_ip, api_port, pancakes, bacon)
response = requests.post(url)
baconpancakes = response.json()
status = response.status_code
pancake_id = baconpancakes['id']
self.assertEqual(201, status)
self.assertEqual('REQUESTED', baconpancakes['status'])
url = url + '/' + pancake_id
for attempt in range(20):
r = requests.get(url)
pancake = r.json()
try:
self.assertEqual('MADE', baconpancakes['status'])
except Exception:
time.sleep(5)
self.assertEqual('MADE', baconpancakes['status'])
self.assertIn('bacon', baconpancakes.keys())
$ galley
Pulling dockerfile/redis:latest from registry.
Checking if image dockerfile/redis:latest exists.
Found image dockerfile/redis:latest.
Successfully pulled dockerfile/redis:latest.
Pulling dockerfile/mongodb:latest from registry.
Checking if image dockerfile/mongodb:latest exists.
Found image dockerfile/mongodb:latest.
Successfully pulled dockerfile/mongodb:latest.
Building image . with tag baconpancakes.
Checking if image c023ce32fc62 exists.
Found image c023ce32fc62.
Successfully built image c023ce32fc62 from ..
Creating dockerfile/redis container.
Successfully created dockerfile/redis container: 380c81fe0775
Creating dockerfile/mongodb container.
Successfully created dockerfile/mongodb container: 706e35d5e28f
Creating c023ce32fc62 container.
Successfully created c023ce32fc62 container: e0cc0d1cebc2
Creating c023ce32fc62 container.
Successfully created c023ce32fc62 container: 1f4d584d8dc0
Starting container 380c81fe0775.
Successfully started container 380c81fe0775.
Starting container 706e35d5e28f.
Successfully started container 706e35d5e28f.
Starting container e0cc0d1cebc2.
Successfully started container e0cc0d1cebc2.
Starting container 1f4d584d8dc0.
Successfully started container 1f4d584d8dc0.
Waiting for containers to start...
...
----------------------------------------------------------------------
Ran 3 tests in 30.617s
OK
Total Elapsed Time: 362.87 seconds.
Requirements:
- Python 2.7.5
- Vagrant
- VirtualBox
- docker-osx
-
Setup VirtualBox and Vagrant.
-
Install docker-osx:
curl https://raw.github.com/noplay/docker-osx/0.8.0/docker-osx > /usr/local/bin/docker-osx chmod +x /usr/local/bin/docker-osx
-
Start docker-osx:
docker-osx start
-
Once the script is done, you should see a line like this:
To use docker: export DOCKER_HOST=tcp://172.16.42.43:4243 and then use the docker command from os-x directly.
Copy and paste the
export DOCKER_HOST=tcp://172.16.42.43:4243
line and run this to set theDOCKER_HOST
environment variable. Galley will need this to communicate with Docker. -
Verify Docker is working:
docker version
-
Proceed to the regular installation instructions.