Skip to content

oscarfh/arquillian-cube

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Arquillian Cube

Warning
1.0.0.Alpha7 breaks incompatibility with previous versions in some cases. The major difference is that instead of using the boot2docker keyword to refer to the auto resolved boot2docker ip in the serverUri parameter, you should now used dockerHost.

What is this?

Arquillian Cube is an Arquillian extension that can be used to manager Docker containers from Arquillian.

Extension is named Cube for two reasons:

  • Because Docker is like a cube

  • Because Borg starship is named cube and well because we are moving tests close to production we can say that "any resistance is futile, bugs will be assimilated".

With this extension you can start a Docker container with a server installed, deploy the required deployable file within it and execute Arquillian tests.

The key point here is that if Docker is used as deployable platform in production, your tests are executed in a the same container as it will be in production, so your tests are even more real than before.

But it also lets you start a container with every required service like database, mail server, …​ and instead of stubbing or using fake objects your tests can use real servers.

Warning

This extension has been developed and tested on a Linux machine with the Docker server already installed. It works with Boot2Docker as well in Windows and MacOS machines, but some parameters like host ip must be the Boot2Docker server instead of localhost (in case you have Docker server installed inside your own machine).

One of the best resources to learn about why using Boot2Docker is different from using Docker in Linux can be read here http://viget.com/extend/how-to-use-docker-on-os-x-the-missing-guide

Preliminaries

Arquillian Cube relies on docker-java API.

To use Arquillian Cube you need a Docker daemon running on a computer (it can be local or not), but probably it will be at local.

By default the Docker server uses UNIX sockets for communicating with the Docker client. Arquillian Cube will attempt to detect the operating system it is running on and either set docker-java to use UNIX socket on Linux or to Boot2Docker on Windows/Mac as the default URI.

If you want to use TCP/IP to connect to the Docker server, you’ll need to make sure that your Docker server is listening on TCP port. To allow Docker server to use TCP add the following line to /etc/default/docker:

DOCKER_OPTS="-H tcp://127.0.0.1:2375 -H unix:///var/run/docker.sock"

After restarting the Docker daemon you need to make sure that Docker is up and listening on TCP.

$ docker -H tcp://127.0.0.1:2375 version

Client version: 0.8.0
Go version (client): go1.2
Git commit (client): cc3a8c8
Server version: 1.2.0
Git commit (server): fa7b24f
Go version (server): go1.3.1

If you cannot see the client and server versions then it means that something is wrong with the Docker installation.

Basic Example

After having a Docker server installed we can start using Arquillian Cube. In this case we are going to use a very simple example using a Docker image with Apache Tomcat and we are going to test a Servlet on it.

HelloWorldServlet.java
@WebServlet("/HelloWorld")
public class HelloWorldServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    PrintWriter writer = resp.getWriter();
    writer.println("Hello World");
  }
}
HelloWorldServletTest.java
@RunWith(Arquillian.class)
public class HelloWorldServletTest {

  @Deployment(testable=false) //(1)
  public static WebArchive create() {
    return ShrinkWrap.create(WebArchive.class, "hello.war").addClass(HelloWorldServlet.class); //(2)
  }

  @Test
  public void should_parse_and_load_configuration_file(@ArquillianResource URL resource) throws IOException { //(3)

    URL obj = new URL(resource, "HelloWorld");
    HttpURLConnection con = (HttpURLConnection) obj.openConnection();
    con.setRequestMethod("GET");

    BufferedReader in = new BufferedReader(
            new InputStreamReader(con.getInputStream()));
    String inputLine;
    StringBuffer response = new StringBuffer();

    while ((inputLine = in.readLine()) != null) {
        response.append(inputLine);
    }
    in.close();

    assertThat(response.toString(), is("Hello World"));//(4)
  }
}
  1. In this case we are running the test as a client. So in fact this test is executed against the container instead of inside the container.

  2. No changes in this part, we need to create a deployable file, and because we are testing against Tomcat, a war file is created.

  3. Because the test is run as client, we can use @ArquillianResource to get the URL where the file is deployed. Note that this will be the URL to access Tomcat running inside the Docker container.

  4. Typical jUnit assertion of servlet response.

Now this test could be run in any container, there is nothing that ties this to Docker. Next step is adding some dependencies apart from the typical Arquillian dependencies.

pom.xml
<dependency>
  <groupId>org.arquillian.cube</groupId>
  <artifactId>arquillian-cube-docker</artifactId> <!--(1)-->
  <version>${project.version}</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.jboss.arquillian.container</groupId>
  <artifactId>arquillian-tomcat-remote-7</artifactId> <!--(2)-->
  <version>1.0.0.CR7</version>
  <scope>test</scope>
</dependency>
  1. Adds Arquillian Cube dependency.

  2. From the point of view of Arquillian, Tomcat is being executed in a remote host (in fact this is true because Tomcat is running inside Docker which is external to Arquillian), so we need to add the remote adapter.

And finally we need to configure Tomcat remote adapter and Arquillian Cube in arquillian.xml file.

arquillian.xml
<?xml version="1.0"?>
<arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://jboss.org/schema/arquillian"
  xsi:schemaLocation="http://jboss.org/schema/arquillian
  http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

  <extension qualifier="docker"> <!--(1)-->
      <property name="serverVersion">1.12</property> <!--(2)-->
      <property name="serverUri">http://localhost:2375</property> <!--(3)-->
      <property name="dockerContainers"> <!--(4)-->
          tomcat:
            image: tutum/tomcat:7.0
            exposedPorts: [8089/tcp]
            await:
              strategy: polling
            env: [TOMCAT_PASS=mypass, JAVA_OPTS=-Dcom.sun.management.jmxremote.port=8089 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false]
            portBindings: [8089/tcp, 8080/tcp]
      </property>
  </extension>

  <container qualifier="tomcat" default="true"> <!--(5)-->
      <configuration>
          <property name="host">localhost</property> <!--(6)-->
          <property name="httpPort">8080</property> <!--(7)-->
          <property name="user">admin</property> <!--(8)-->
          <property name="pass">mypass</property>
      </configuration>
  </container>

</arquillian>
  1. Arquillian Cube extension is registered.

  2. Docker server version is required.

  3. Docker server URI is required. In case you are using a remote Docker host or Boot2Docker here you need to set the remote host ip, but in this case Docker server is on same machine.

  4. A Docker container contains a lot of parameters that can be configured. To avoid having to create one XML property for each one, a YAML content can be embedded directly as property.

  5. Configuration of Tomcat remote adapter. Cube will start the Docker container when it is ran in the same context as an Arquillian container with the same name.

  6. Host can be localhost because there is a port forwarding between container and Docker server.

  7. Port is exposed as well.

  8. User and password are required to deploy the war file to remote Tomcat.

And that’s all. Now you can run your test and you will see how tutum/tomcat:7.0 image is downloaded and started. Ports 8080 (Tomcat standard port) and 8089(JMX port used by Arquillian) are exposed. Finally in env section, environment variables are set. Read next link to understand why this is required https://docs.jboss.org/author/display/ARQ/Tomcat+7.0+-+Remote

Configuration

Arquillian Cube requires some parameters to be configured, some related with Docker server and others related on the image that is being used. Let’s see valid attributes:

serverVersion

Version of REST API provided by_Docker_ server. You should check on the Docker site which version of REST API is shipped inside installed Docker service. This field is not mandatory and if it’s not set the default provided version from docker-java will be used.

serverUri

Uri of Docker server. If the Docker server is running natively on Linux then this will be an URI pointing to localhost docker host but if you are using Boot2Docker or a remote Docker server then the URI should be changed to point to the Docker remote URI. It can be a unix socket URI as well in case you are running Docker on Linux (unix:///var/run/docker.sock). Also you can read at this section about automatic resolution of serverUri parameter. Also you can use DOCKER_HOST java property or system environment to set this parameter.

dockerRegistry

Sets the location of Docker registry. Default value is the official Docker registry located at https://registry.hub.docker.com

username

Sets the username to connect to Docker registry.

password

Sets the password to connect to Docker registry.

dockerContainers

Each Docker image (or container) can be configured with different parameters. This configuration is provided in YAML format. This property can be used to embed as YAML string value, all configuration.

dockerContainersFile

Instead of embedding YAML as a string, you can set the location of a YAML file with this attribute. The location can be a relative from the root of the project or also a URI that is converted to URL so you can effectively have docker definitions on remote sites.

dockerContainersFiles

You can set a list of locations separated by comma. These locations follow the same rules as dockerContainersFile so it can be a file or an URI. This property can be used to append the definitions from several files.

definitionFormat

Sets the format of content expressed in dockerContainers attribute or in file set in dockerContainersFile. It can contain two possible values CUBE (default one in case of not set) to indicate that content is written following Arquillian Cube format or COMPOSE to indicate that content is written following Docker Compose format.

autoStartContainers

Cube will normally start a Docker container when it has the same name as an active Arquillian container and all the containers defined as links to this container, so basically Cube resolves all the container depdendencies as well e.g. a database where the application saves data, or mail server where application sends an email. That works for things that are DeployableContainer's. For any other container services that might not have a link to the DeployableContainer, e.g. a monitor, you can use the autoStartContainers option to define which Docker containers to automatically start up. The option takes a comma separated list of Docker container ids. e.g. monitor. Arquillian Cube will attempt to start the containers in parallel if possible as well as start any linked containers. Also if you need to start several images, instead of adding them as CSV, you can use a regular expression by prefixing with regexp:, for example setting the property to regexp:a(.*) would start all container ids starting with a.

certPath

Path where certificates are stored. If you are not using https protocol this parameter is not required. This parameter accepts starting with ~ as home directory

boot2dockerPath

Sets the full location (and program name) of boot2docker. For example /opt/boot2dockerhome/boot2docker.

dockerMachinePath

Sets the full location (and program name) of docker-machine. For example /opt/dockermachinehome/docker-machine.

machineName

Sets the machine name in case you are using docker-machine to manage your docker host. This parameter is mandatory when using docker-machine with more than one running machine. In case of having only one docker machine running, it is not necessary to set it since it is auto resolved by cube.

dockerInsideDockerResolution

Boolean to set if Cube should detect that tests are run inside an already started Docker container, so Docker containers started by Cube could be run using DinD (Docker Inside Docker) or DoD (docker On Docker). Basically it ignores any SERVER_URI, Boot2Docker or docker-machine properties and sets the serverUri to unix:///var/run/docker.sock. By default its value is true. If you want to use an external dockerhost, then you need to set this property to false.

connectionMode

Connection Mode to bypass the Create/Start Cube commands if the a Docker Container with the same name is already running on the target system. This parameter can receive three possible values. STARTANDSTOP which is the default one if not set any and simply creates and stops all Docker Containers. If a container is already running, an exception is thrown. STARTORCONNECT mode tries to bypass the Create/Start Cube commands if a container with the same name is already running, and if it is the case doesn’t stop it at the end. But if container is not already running, Cube will start one and stop it at the end of the execution. And last mode is STARTORCONNECTANDLEAVE which is exactly the same of STARTORCONNECT but if container is started by Cube it won’t be stopped at the end of the execution so it can be reused in next executions.

Some of these properties can be provided by using standard Docker system environment variables so you can set once and use them in your tests too. Moreover you can set as Java system properties (-D…​) as well.

serverUri

DOCKER_HOST

certPath

DOCKER_CERT_PATH

machineName

DOCKER_MACHINE_NAME

In the next example you can see a whole YAML document with configuration properties. Keep in mind that almost all of them are configuration parameters provided by Docker remote API. In this example we are going to explain the attributes that are most used and special cases. Of course not all of them are mandatory:

Note
In YAML adding brackets ("[" "]") is for setting a list.
tomcat: #1
  image: tutum/tomcat:7.0 #2
  exposedPorts: [8089/tcp] #3
  await: #4
    strategy: polling #5
  workingDir: .
  alwaysPull: false
  disableNetwork: true
  hostName: host
  portSpecs: [80,81]
  user: alex
  tty: true
  stdinOpen: true
  stdinOnce: true
  memoryLimit: 1
  memorySwap: 1
  cpuShares: 1
  cpuSet: a
  extraHosts: a
  attachStdin: true
  attachStderr: true
  env: [TOMCAT_PASS=mypass, JAVA_OPTS=-Dcom.sun.management.jmxremote.port=8089] #6
  cmd: [] #7
  dns: [127.0.0.1]
  volumes: [/tmp]
  volumesFrom: [tomcat]
  binds:
    - /host:/container:ro
  links:
    - name:alias
    - name2:alias2
  portBindings: [8089/tcp, 8081->8080/tcp] #8
  privileged: true
  publishAllPorts: true
  networkMode: host
  dnsSearch: [127.0.0.1]
  entryPoint: [sh]
  devices:
    cGroupPermissions: a
    pathOnHost: b
    pathInContainer: c
  restartPolicy: #10
    name: failure
    maximumRetryCount: 1
  capAdd: [a]
  capDrop: [b]
  extends: container-id #9
  1. The name that are going to be assign to running container. It is mandatory.

  2. The name of the image to be used. It is mandatory. If the image has not already been pulled by the Docker server, Arquillian Cube will pull it for you. If you want to always pull latest image before container is created, you can configure alwaysPull: true.

  3. Sets exposed ports of the running container. It should follow the format port number slash(/) and _protocol (udp or tcp). Note that it is a list and it is not mandatory.

  4. After a container is started, it starts booting up the defined services/commands. Depending on the nature of service, the lifecycle of these services are linked to start up or not. For example Tomcat, Wildlfy, TomEE and in general all Java servers must be started in foreground and this means that from the point of view of the client, the container never finishes to start. But on the other side other services like Redis are started in background and when the container is started you can be sure that Redis server is there. To avoid executing tests before the services are ready, you can set which await strategy should be used from Arquillian Cube side to accept that Docker container and all its defined services are up and ready. It is not mandatory and by default polling with ss command strategy is used.

  5. In strategy you set which strategy you want to follow. Currently three strategies are supported. static, native and polling.

  6. You can pass environment variables by using env. In this section you can set special dockerServerIp string which at runtime will be replaced by Cube to current docker server ip.

  7. After the container is up, a list of commands can be executed within it.

  8. Port forwarding is configured using portBinding section. It contains a list of exposedPort and port separated by arrow (). If only one port is provided, Arquillian Cube will expose the same port number. In this example the exposed port 8089 is mapped to 8089 and exposed port 8080 is mapped to 8081.

  9. You can extend another configuration. Any top level element and it’s children from the target container-id will be copied over to this configuration, unless they have been defined here already.

As we’ve seen in the basic example the definition of the Arquillian Cube scenarios are described in dockerContainers property. But if you want you can avoid using this property by simply creating a file called cube in the root of the classpath of your project. Arquillian Cube will read it as if it was defined in arquilllian.xml file.

src/test/resources/cube
tomcat:
  image: tomcat:7.0
  exposedPorts: [8089/tcp]
  await:
    strategy: polling
  env: [TOMCAT_PASS=mypass, JAVA_OPTS=-Dcom.sun.management.jmxremote.port=8089 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false]
  portBindings: [8089/tcp, 8080/tcp]

cube.environment

In case you use the dockerContainersFile to configure a YAML file (Cube format or Compose format) for loading the definition of your containers, you can use cube.environment system property to append a prefix to the file you want to load.

For example, in case you set dockerContainersFile to docker-compose.yml and cube.environment not set, Cube loads a file named docker-compose.yml. But if cube.environment is set to qa (-Dcube.environment=qa), the Cube loads a file called docker-compose.qa.yml.

This property is useful for loading different configurations depending on the environment you are running the tests.

Await

After a container is started, it starts booting up the defined services/commands. Depending on the nature of service, the lifecycle of these services are linked to start up or not. For example Tomcat, Wildlfy, TomEE and in general all Java servers must be started in foreground and this means that from the point of view of the Docker client, the container never finishes to start. But on the other side other services like Redis are started in background and when the container is started you can be sure that Redis server is there. To avoid executing tests before the services are ready, you can set which await strategy should be used from Arquillian Cube side to accept that Docker container and all its defined services are up and ready.

Currently next await strategies are supported:

native

it uses wait command. In this case current thread is waiting until the Docker server notifies that has started. In case of foreground services this is not the approach to be used.

polling

in this case a polling (with ping or ss command) is executed for 5 seconds against all exposed ports. When communication to all exposed ports is acknowledged, the container is considered to be up. This approach is the one to be used in case of services started in foreground. By default polling executes ss command inside the running container to know if the server is already running. You can use a ping from client by setting type attribute to ping; Note that ping only works if you are running Docker daemon on localhost. In almost all cases the default behaviour matches all scenarios. If it is not specified, this is the default strategy.

static

similar to polling but it uses the host ip and specified list of ports provided as configuration parameter. This can be used in case of using Boot2Docker.

sleeping

sleeps current thread for the specified amount of time. You can specify the time in seconds or milliseconds.

By default in case you don’t specify any await strategy, polling with ss command is used.

Example native
tomcat:
  image: tutum/tomcat:7.0
  exposedPorts: [8089/tcp]
  await:
    strategy: native
Example polling using ss command by default
tomcat:
  image: tutum/tomcat:7.0
  exposedPorts: [8089/tcp]
  await:
    strategy: polling
    sleepPollingTime: 200 s #1
    iterations: 3 #2
  1. Optional parameter to configure sleeping time between poling. You can set in seconds using s or miliseconds using ms. By default time unit is miliseconds and value 500.

  2. Optional parameter to configure number of retries to be done. By default 10 iterations are done.

Example static
tomcat:
  image: tutum/tomcat:7.0
  exposedPorts: [8089/tcp]
  await:
    strategy: static
    ip: localhost
    ports: [8080, 8089]
Example sleeping
tomcat:
  image: tutum/tomcat:7.0
  exposedPorts: [8089/tcp]
  await:
    strategy: sleeping
    sleepTime: 200 s #1
  1. Optional parameter to configure sleeping time between poling. You can set in seconds using s or miliseconds using ms. By default time unit is miliseconds and value 500.

Inferring exposedPorts from portBinding

When you are using Docker you can set two different parameters, exposedPort and portBinding. exposedPorts are ports that are available inside Docker infrastructure and they are used for communication between Docker containers but not from outside. On the other side portBindings are a way to instruct Docker container to publish a port to be available from outside (for example from our test).

It seems reasonable that if you set a port binding this port should automatically be exposed port as well. For this reason in Arquillian Cube you can use portBinding and it will automatically set to exposed port.

In next example we are only setting portBinding and Arquillian Cube will instruct Docker to expose port 8080 and of course bind the port 8080 so it can be accessible from outside.

arquillian.xml
daytime:
  buildImage:
    dockerfileLocation: src/test/resources/undertow
    noCache: true
    remove: true
  await:
    strategy: polling
  portBindings: [8080/tcp]

Also it is not necessary to set the network protocol (tcp or udp). If protocol is not specified portBindings: ["8080"] then tcp is used by default. Notice that you need to add double-quotes to stringify the value.

Allow connecting to a running container

With the configuration option connectionMode you can bypass the Create/Start Cube commands if the a Docker Container with the same name is already running on the target system. If so, Arquillian Cube will reuse that Container moving forward.

This allows you to prestart the containers manually during development and just connect to them to avoid the extra cost of starting the Docker Containers for each test run. This assumes you are not changing the actual definition of the Docker Container itself.

An example of this configuration could be:

arquillian.xml
<extension qualifier="docker">
  <property name="serverVersion">1.12</property>
  <property name="serverUri">http://localhost:2375</property>
  <property name="connectionMode">STARTORCONNECT</property>
  <property name="dockerContainers">
      tomcat:
        #more properties
  </property>
</extension>

connectionMode is an attribute that can receive three parameters:

STARTANDSTOP

it is the default one if not set any and simply creates and stops all Docker Containers. If a container is already running, an exception is thrown.

STARTORCONNECT

it tries to bypass the Create/Start Cube commands if a container with the same name is already running, and if it is the case doesn’t stop it at the end. But if container is not already running, Cube will start one and stop it at the end of the execution.

STARTORCONNECTANDLEAVE

it is exactly the same of STARTORCONNECT but if container is started by Cube it won’t be stopped at the end of the execution so it can be reused in next executions.

Before Stop Events

Sometimes when the tests has finished and container is stopped you want to inspect some data like container console or getting a file from the container to manual inspecting. In these cases you can configure each container to copy console log or copy a file/s from container to local machine just before container is stopped.

Next snippet shows how to copy a directory from container to local disk:

tomcat_default:
  image: tutum/tomcat:7.0
  beforeStop: # (1)
    - copy: # (2)
        from: /test
        to: /tmp

    - log: # (3)
        to: /tmp/container.log
  1. beforeStop goes into the container section and may contain a list of copy and log elements.

  2. copy is used to notify that we want to copy some directories or files form from container location to to local location.

  3. log is used to notify that we want to copy container log to to local location.

In case of log command the standard output and the error output are returned. log Docker command can receive some configuration paramters and you can set them too in configuration file.

Example of log parameters
beforeStop:
  - log:
    to: /tmp/container.log
    follow: true
    stdout: true
    stderr: false
    timestamps: true
    tail: 10

Automatic serverUri resolution

serverUri parameter is where you configure the Uri of Docker server. This parameter is not mandatory and in case you don’t set it, Arquillian Cube will use next values:

Linux

unix:///var/run/docker.sock

Windows

https://dockerHost:2376

MacOS

https://dockerHost:2376

Docker Machine

https://dockerHost:2376

Boot2Docker and Docker Machine

If you are using boot2docker or docker machine there are some parameters that depends on the local installation. For example boot2docker ip is not localhost and may change every time you start a new boot2docker instance. Also every time you start boot2docker copies required certificates to home directory of local machine.

Arquillian Cube offers some automatic mechanisms to use boot2docker or docker machine in Cube.

The first one is that serverUri parameter can contain the word dockerHost like for example https://dockerHost:2376. When Cube is started it will check if the serverUri contains the dockerHost word, and if it is the case it will do next things:

  1. if docker machine name is provided by using machineName property then Docker Machine command is run to get the ip to be replaced in dockerHost.

  2. if previous conditions are not met, then boot2docker command is run to get the ip to be replaced in dockerHost.

Important
In case of using docker machine with only one machine running, it is not necessary to use machineName property since Cube will be able to resolve it automatically.

Boot2Docker

In case of boot2docker it will run the command boot2docker ip to get the ip and substitute the dockerHost keyword to the ip returned by that command.

Note that by default Arquillian Cube assumes that boot2docker command is on PATH, but you can configure its location by using boot2dockerPath property which is the full location (and program name) of boot2docker. For example /opt/boot2dockerhome/boot2docker.

boot2docker runs in https and you need to set the certificates path. These certificates are copied by boot2docker by default at <HOME>/.boot2docker/certs/boot2docker-vm. If this property is not set and the serverUri contains dockerHost, then this property is automatically configured to <HOME>/.boot2docker/certs/boot2docker-vm so you don’t need to worry to set for each environment.

Docker Machine

In case of docker-machine it will run the command docker-machine ip <machineName> to get the ip and substitute the dockerHost keyword to the ip returned by that command.

Note that by default Arquillian Cube assumes that docker-machine command is on PATH, but you can configure its location by using the dockerMachinePath proeprty which is the full location (and program name too) of docker-machine. For example /usr/bin/docker-machine..

docker-machine can run with boot2docker together. And this docker host instance runs in https so you need to set the certificates path. These certificates are copied by docker-machine by default at <HOME>/.docker/machine/machines. If this property is not set and docker-machine is run, then this property is automatically configured to default location, so you don’t need to worry to set for each environment.

For example you can configure arquillian.xml file to use docker-machine as:

arquillian.xml
<extension qualifier="docker">
    <property name="serverVersion">${docker.api.version}</property>
    <property name="definitionFormat">COMPOSE</property>
    <property name="machineName">dev</property> <!-- 1 -->
    <property name="dockerContainersFile">docker-compose.yml</property>
</extension>
  1. Sets docker machine to dev.

Notice that you only need to add machineName property in case you have more than one machine running, everything else it is exactly the same as previous examples.

Building containers

To build a container Docker uses a file called Dockerfile http://docs.docker.com/reference/builder/. Arquillian Cube also supports building and running a container from a Dockerfile.

To set that Arquillian Cube must build the container, the image property must be changed to buildImage and add the location of Dockerfile.

Let’s see previous example but instead of creating a container from a predefined image, we are going to build one:

arquillian.xml
<property name="dockerContainers">
  tomcat:
    buildImage: #1
      dockerfileLocation: src/test/resources-tomcat-7-dockerfile/tomcat #2
      noCache: true #3
      remove: true #4
      dockerfileName: my-dockerfile #5
    await:
      strategy: polling
    env: [JAVA_OPTS=-Dcom.sun.management.jmxremote.port=8089 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false]
    portBindings: [8089/tcp, 8080/tcp]
</property>
  1. buildImage section is used in front of image. In case of both sections present in a document, image section has preference over buildImage.

  2. dockerfileLocation contains the location of Dockerfile and all files required to build the container.

  3. Property to enable or disable the no cache attribute.

  4. Property to enable or disable the remove attribute.

  5. Property to set the dockerfile name to be used instead of the default ones.

Tip
dockerfileLocation can be a directory that must contains Dockerfile in root directory (in case you don’t set dockerfileName property), also a tar.gz file or a URL pointing to a tar.gz file.

An example of Dockerfile is:

src/test/resources-tomcat-7-dockerfile/tomcat/Dockerfile
FROM tutum/tomcat:7.0

ENV JAVA_OPTS -Dcom.sun.management.jmxremote.port=8089 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
ADD tomcat-users.xml /tomcat/conf/ # (1)
EXPOSE 8089
CMD ["/tomcat/bin/catalina.sh","run"]
  1. tomcat-users.xml file is located at same directory as Dockerfile.

Docker-Compose Format

Instead of using Arquillian Cube format, you can use Docker Compose format to define containers layout. This means that you can use the same Docker Compose file for running your tests with Arquillian Cube and without any change run docker-compose up command from terminal and get the same result.

It is important to note that this is not a docker-compose implementation but only the docker-compose format. This means that for example you cannot execute some CLI commands of docker-compose like start several instances of same service.

In case of some specific Arquillian Cube attributes like await strategy cannot be configured and the default values are going to be used.

Moreover there are some docker-compose commands that are not implemented yet due to restrictions on docker-java library. These commands are pid, log_driver and security_opt. But they will be implemented as soon as docker-java library adds their support.

Last thing, in case you define a command that is not implemented in Arquillian Cube, this command will be ignored (no exception will be thrown), but a log line will be printed notifying this situation. Please it is really important that if this happens you open a bug so we can add support for them. Althought this warning we will try to maintain aligned with the latest docker-compose format.

Let’s see how you can rewrite previous HelloWorld example with Tomcat to be used using docker-compose format.

First let’s create a file called envs on root of the project which configures environment variables:

envs
TOMCAT_PASS=mypass
JAVA_OPTS=-Djava.rmi.server.hostname=dockerServerIp -Dcom.sun.management.jmxremote.rmi.port=8088 -Dcom.sun.management.jmxremote.port=8089 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
Important
You can use dockerServerIp as special tag for identify the docker host IP. Before injecting the environement variables, Cube will change it to real host IP.

Then you can create a file called docker-compose.yml following docker-compose commands on root of the project:

docker-compose.yml
tomcat:
  env_file: envs
  image: tutum/tomcat:7.0
  ports:
      - "8089:8089"
      - "8088:8088"
      - "8081:8080"

and finally you can configure in arquillian.xml file that you want to use docker-compose format.

src/test/resources/arquillian.xml
<extension qualifier="docker">
  <property name="serverVersion">1.13</property>
  <property name="serverUri">localhost</property>
  <property name="definitionFormat">COMPOSE</property>
  <property name="dockerContainersFile">docker-compose.yml</property>
</extension>

<container qualifier="tomcat">
  <configuration>
    <property name="user">admin</property>
    <property name="pass">mypass</property>
  </configuration>
</container>

And that’s all, you can now reuse your existing docker-compose files in Arquillian Cube too. You can see the full example at: https://github.com/arquillian/arquillian-cube/tree/master/docker/ftest-docker-compose

Enrichers

Arquillian Cube comes with a few enrichers.

One for injecting the CubeID(containerId) of the current container created for executing the test, one that injects the CubeController to call lifecycle methods on any cube and one that injects com.github.dockerjava.api.DockerClient instance used to communicate with Docker server. Also you can inject in your tests the Docker Host IP used for running containers by using @HostIp annotation.

DockerClient and Docker Host IP injection only work if the tests are run in client mode, that is by using @RunAsClient or by setting the testable property to false @Deployment(testable = false).

These can be injected using the @ArquillianResource annotation.

As examples:

CubeIDResourceProvider.java
@ArquillianResource
CubeID containerId;
CubeResourceProvider.java
@ArquillianResource
DockerClient dockerClient;
CubeControllerProvider.java
@ArquillianResource
CubeController cubeController;
DockerHostProvider.java
@HostIp
String ip;

Docker Inside Docker / Docker On Docker

If you are running your tests inside your continuous integration/delivery server (for example in Jenkins) and at the same time the server is running inside Docker. Then the docker containers started for Cube are run inside a Docker container. So you effectively face the Docker inside Docker problem.

From Arquillian Cube perspective we cannot do a lot of things, more then adapting to this situation by changing the serverUri. Basically it ignores any SERVER_URI, Boot2Docker or docker-machine properties and sets the serverUri to unix:///var/run/docker.sock.

You can avoid this behaviour by setting dockerInsideDockerResolution to false.

INFO: In this case almost all work should be done in infrastructure level by configuring correctly Docker instances. For this reason it is important that before running Cube tests, you test manually your infrastructure to be sure that everything is connected as expected.

In next sections, some minor information is provided on how to run Docker inside/on Docker. Keep in mind that you need to configure continuous integration/delivery correctly.

Docker Inside Docker

You can find more information about Docker Inside Docker at: https://github.com/jpetazzo/dind

Also if you are using Jenkins you can use next Jenkins Slave. kmadel/dind-jenkins-slave:1.4 running with privileged flag.

Docker On Docker

If instead of running Docker inside Docker, you want to use the Docker instance/host of the "parent" Docker, you can map as volume the Docker CLI, Docker socket and apparmor library from parent to child container.

-v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker -v /usr/lib/x86_64-linux-gnu/libapparmor.so.1.1.0:/usr/lib/x86_64-linux-gnu/libapparmor.so.1

CubeController

CubeController is facade class that let’s you interact with any cube container. It offers some operations like creating and destroying Cubes (in case of Docker, it is Docker containers), copy a directory to local directory, get a log with all the changes that happened to Cube filesystem, execute a Top command or copy the logs to local file.

Suppose you have next Docker Container definition in dockerContainers property:

arquillian.xml
<property name="dockerContainers">
    manual_database:
        image: zhilvis/h2-db
        portBindings: [1521/tcp, 8181->81/tcp]
</property>

If you enrich your test with CubeController then you will be able to:

  • call cubeController.create("manual_database") to create the Cube defined in dockerContainers with name manual_database.

  • call cubeController.start("manual_database") to start the given Cube.

  • call cubeController.stop("manual_database") to stop the given Cube.

  • call cubeController.destroy("manual_database") to destroy the given Cube.

But also it offers some extra operations not related with the lifecycle of a Cube.

  • cubeController.copyFileDirectoryFromContainer("manual_database", "/db", newFolder.getAbsolutePath()) to copy content from container folder /db to newFolder local location.

  • List<ChangeLog> changesOnFilesystem = cubeController.changesOnFilesystem("manual_database") to returns a log with all changes that has occurred inside given Cube.

  • TopContainer top = cubeController.top("manual_testing") to get the result of executing top command inside Cube.

  • cubeController.copyLog("manual_testing", follow, stdout, stderr, timestamp, tail, byteArrayOutputStream) to copy Cube log to given outputStream. This operation only works in Client mode.

Auto starting Cubes outside of Arquillian Containers

Probably any application you may write will need an application/servlet container but also other servers like database server or mail server. Each one will be placed on one Docker Container. So for example a full application may contain one Docker Container with an application server (for example Wildfly) and another container with a database (for example H2).

Arquillian Cube can orchestrate these containers as well.

An example of orchestration can be:

arquillian.xml
<property name="dockerContainers">
  wildfly_database:
    extends: wildfly
    links:
      - database:database #1
  database:
    image: zhilvis/h2-db
    exposedPorts: [81/tcp, 1521/tcp]
    await:
      strategy: polling
    portBindings: [1521/tcp, 8181->81/tcp]
  </property>

<container qualifier="wildfly_database">
  <configuration>
    <property name="target">wildfly:8.1.0.Final:remote</property>
    <property name="username">admin</property>
    <property name="password">Admin#70365</property>
  </configuration>
</container>
  1. We use link property to connect Wildfly container to database container.

In this case when a test is started both containers are started and when both are ready to receive requests, the test will be executed.

And the database definition shall be:

UserRepository.java
@DataSourceDefinition(
  name = "java:app/TestDataSource",
  className = "org.h2.jdbcx.JdbcDataSource",
  url = "jdbc:h2:tcp://database:1521/opt/h2-data/test",
  user = "sa",
  password = "sa"
)
@Stateless
public class UserRepository {

    @PersistenceContext
    private EntityManager em;

    public void store(User user) {
        em.persist(user);
    }
}

Cube will normally start a Docker container when it has the same name as an active Arquillian container and all the links from this container to another containers as we have seen in previous example. Basically Cube resolves all the container depdendencies as well e.g. a database where the application saves data, or mail server where application sends an email.

That works for things that are DeployableContainer’s. For any other container services that might not have a link to the DeployableContainer, e.g. a monitor, you can use the autoStartContainers option to define which Docker containers to automatically start up. The option takes a comma separated list of Docker container ids. e.g. monitor. Arquillian Cube will attempt to start the containers in parallel if possible as well as start any linked containers. Also if you need to start several images, instead of adding them as CSV, you can use a regular expression by prefixing with regexp:, for example setting the property to regexp:a(.*) would start all container ids starting with a.

For example:

arquillian.xml
<extension qualifier="docker">
    <property name="autoStartContainers">regexp:a(.*)</property>
</extension>

Auto-Remapping

Arquillian Cube can automatically configure default ports of container in case of port forwarding.

What Arquillian Cube does internally is remapping default DeployableContainer port values to the ones configured in Docker Containers configuration.

Suppose you have a Docker Container configuration like:

arquillian.xml
<property name="dockerContainers">
    tomcat_default:
      image: tutum/tomcat:7.0
      exposedPorts: [8089/tcp]
      await:
        strategy: polling
      env: [TOMCAT_PASS=mypass, JAVA_OPTS=-Dcom.sun.management.jmxremote.port=8089 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false]
      portBindings: [8089/tcp, 8081->8080/tcp] #1
</property>
  1. Note that the exposed port is the 8081.

Then in theory you should configure the remote Tomcat adapter to port 8081 on your arquillian.xml file. But let’s say that you are using that remote adapter for a remote local machine Tomcat (outside Docker) too, and is configured to use 8080 port.

arquillian.xml
<container qualifier="tomcat_default">
  configuration>
    <property name="host">localhost</property>
    <property name="user">admin</property>
    <property name="pass">mypass</property>
  </configuration>
</container>

Which basically uses default port (8080) to connect to remote server.

In this case you don’t need to create a new container tag, Arquillian Cube is smart enough to change the default port value automatically; in case of Tomcat 8080 to 8081. Arquillan Cube will apply autoremapping to all properties that contains port as a substring of the property, and will remap if it is necessary.

Note
Automapping only works in case you want to change the default server port to a Docker port forwarded port.

DockerServerIp and Containers

If you are using a remote docker server (not on localhost) or for example boot2docker you may want to set that ip to Arquillian remote adapter configuration so it can deploy the archive under test. In these cases you can hardcode this ip to Arquillian container adapter configuration or you can use the special tag dockerServerIp. At runtime these tag will be replaced by Arquillian Cube to docker server ip configured in serverUri parameter. This replacement only works in properties that contains the string host or address in property name.

So for example:

arquillian.xml
<extension qualifier="docker">
        <property name="serverUri">http://192.168.0.2:2756</property> <!--(1)-->
  ...
</extension>
<container qualifier="tomcat_default">
  configuration>
    <property name="host">dockerServerIp</property> <!--(2)-->
    <property name="user">admin</property>
    <property name="pass">mypass</property>
  </configuration>
</container>
  1. We set the serverUri as usually.

  2. dockerServerIp is replaced at runtime.

The host property will be replaced automatically to 192.168.0.2.

Note
This also works in case you set serverUri using boot2docker special word or by using the defaults. Read more about it Boot2Docker section and Automatic serverUri resolution section.

In case of using unix socket dockerServerUri is replaced to localhost.

Also Arquillian Cube can help you in another way inferring boot2docker ip. In case you are running in MACOS or Windows with boot2docker, you may not need to set host property at all nor using dockerServerIp. Arquillian Cube will inspect any property in configuration class that contains the word address or host that it is not overriden in arquillian.xml and it will set the boot2docker server automatically.

So previous example could be modified to:

arquillian.xml
<container qualifier="tomcat_default">
  configuration>
    <property name="user">admin</property>
    <property name="pass">mypass</property>
  </configuration>
</container>

And in case you are running on Windows or MacOS, `host`property will be automatically set to the +boot2docker +ip.

Container Object pattern

If you search for a description of what Page Object is, you’ll find that The Page Object Pattern gives us a common sense way to model content in a reusable and maintainable way.

And also points that: Within your web app’s UI there are areas that your tests interact with. A Page Object simply models these as objects within the test code. This reduces the amount of duplicated code and means that if the UI changes, the fix need only be applied in one place.

As you can see, Page Object applies to UI elements. We (the Arquillian community) has coined a new pattern following Page Object pattern called Container Object pattern. You can think about Container Object as areas of a container (for now Docker container) that your test might interact with. For example some of these areas could be:

  • to get the host IP where container is running

  • the bounded port for a given exposed port

  • any parameter configured inside the configuration file (Dockerfile) like a user or password to access to the service which the container exposes.

In case of running a MySQL database in the container, it could be the user and password to access to database. Notice that nothing prevents you to generate the correct URL for accessing to the service from the test, or execute commands against container like retrieving an internal file.

And of course as Page Object does, Container Object gives you a way to build a model content that can be reused for several projects.

Before looking at how this pattern is implemented in Cube, let’s go thorough an example:

Suppose all of your applications need to send a file to an FTP server. To write an integration/component test you might need a FTP server to send the file and check that the file was correctly sent. One way to do this is using Docker to start a FTP server just before executing the test, then execute the test using this docker container for FTP server, before stopping the container check that the file is there, and finally stop the container.

So all these operations that involves the FTP server and container could be joined inside a Container Object. This container object might contain information of:

  • which image is used

  • IP and bounded port of host where this FTP server is running

  • user and password to access to the FTP server

  • methods for asserting the existence of a file

Then from the point of view of test, it only communicate with this object instead of directly hard coding all information inside the test. Again as in Page Object, any change on the container only affects the Container Object and not the test itself.

Now let’s see how Arquillian Cube implements Container Object pattern.

Arquillian Cube and Container Object

Let’s see a simple example on how you can implement a Container Object in Cube. Suppose you want to create a container object that encapsulates a ping pong server running inside Docker. The Container Object will be like a simple POJO with special annotations:

PingPongContainer.java
package org.superbiz.containerobject;

@Cube(value = "pingpong", portBinding = "5000->8080/tcp") // (1)
@CubeDockerFile
public class PingPongContainer {

  @HostIp // (2)
  String dockerHost;

  @HostPort(8080)
  private int port;

  public URL getConnectionUrl() { // (3)
    try {
      return new URL(“http://” + dockerHost + “:” + port);
	  } catch (MalformedURLException e) {
		  throw new IllegalArgumentException(e);
	  }
  }
}
  1. @Cube annotation configures Container Object

  2. A Container Object can be enriched with Arquillian enrichers

  3. Container Object hides how to connect to PingPong server.

@Cube annotation is used to configure this Container Object. Initially you set that the started container will be named pingpong and the port binding information for the container instance, in this case 5000→8080/tcp. Notice that this can be an array to set more than one port binding definition.

Next annotation is @CubeDockerFile which configure how Container is created. In this case using a Dockerfile located at default class path location. The default location is the package+classname, so for example in previous case, Dockerfile should be placed at org/superbiz/containerobject/PingPongContainer. Of course you can set any other class path location by passing as value of the annotation.

Important
CubeDockerFile annotation sets the location where the Dockerfile is found and not the file itself. Also this location should be reachable from ClassLoader, so it means it should be loaded in class path in order to find it.

Any Cube can be enriched with any client side enricher, in this case with @HostIp enricher, but it could be enriched with DockerClient using @ArquillianResource as well.

Finally the @HostPort is used to translate the exposed port to bound port. So in this example port value will be 5000. You are going to lean briefly why this annotation is important.

And then you can start using this container object in your test:

PingPongTest.java
@RunWith(Arquillian.class)
public class PingPongTest {

    @Cube
    PingPongContainer pingPongContainer;

    @Test
    public void shouldReturnOkAsPong() throws IOException {
        String pong = ping();
        assertThat(pong, containsString("OK"));
        assertThat(pingPongContainer.getConnectionPort(), is(5000));
    }
}

The most important thing here is that you need to set Container Object as a field of the class and annotate with @Cube. It is very important to annotate the field with Cube, so before Arquillian runs the test, it can detect that it needs to start a new Cube (Docker container), create the Container Object and inject it in the test. Notice that this annotation is exactly the same as used when you defined the Container Object. And it is in this way because you can override any property of the Container Object from the test side. This is why @HostPort annotation is important, since port can be changed from the test definition, you need to find a way to inject the correct port inside the container object.

Important
Container Object pattern only works in Client mode or Arquillian standalone.

ShrinkWrap Dockerfile Descriptor

If you want you can use ShrinkWrap Dockerfile descriptor to create the Dockerfile file. First thing you need to do is adding shrinkwrap-descriptord-api-docker dependencies:

pom.xml
<dependency>
  <groupId>org.jboss.shrinkwrap.descriptors</groupId>
  <artifactId>shrinkwrap-descriptors-api-docker</artifactId>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.jboss.shrinkwrap.descriptors</groupId>
  <artifactId>shrinkwrap-descriptors-impl-docker</artifactId>
  <scope>test</scope>
</dependency>

And in similar way you use @Deployment in Arquillian test, you can use @CubeDockerFile annotation in a public static method to define Dockerfile file and elements required for creating the container programmatically:

PingPongContainer.java
@Cube(value = "pingpong", portBinding = "5000->8080/tcp")
public class PingPongContainer {

  @CubeDockerFile // (1)
  public static Archive<?> createContainer() { // (2)
    String dockerDescriptor =   Descriptors.create(DockerDescriptor.class).from("jonmorehouse/ping-pong").exportAsString();
      return ShrinkWrap.create(GenericArchive.class)
                .add(new StringAsset(dockerDescriptor), "Dockerfile"); // (3)
  }
}
  1. @CubeDockerFile annotation is used.

  2. Method must be public and static.

  3. Returns a GenericArchive with all elements required for building the Docker container instance.

In Arquillian Cube we are providing a org.arquillian.cube.impl.shrinkwrap.asset.CacheUrlAsset asset. This asset is like org.jboss.shrinkwrap.api.asset.UrlAsset but it caches to disk for an amount of time the content that has been downloaded from the URL. By default this expiration time is 1 hour but it is configurable by using proper constructor.

A Container Object can contain more Container Objects inside. So effectively a Container Object can be an aggregation of other Container Objects:

FirstContainerObject.java
@Cube
public class FirstContainerObject {

  @Cube("inner") // (1)
  LinkContainerObject linkContainerObject;

}
  1. Cube definition inside another Cube.

In this case Arquillian Cube will create a link from FirstContainerObject to LinkContainerObject with link value inner:inner.

But of course you can override the link value using @Link annotation.

@Cube("inner")
@Link("db:db")
TestLinkContainerObject linkContainerObject;

Image

So far, you’ve seen that the Container Object creates a Container object from a Dockerfile using @CubeDockerFile annotation, but you can also creates a Container Object from an image by using @Image annotation:

ImageContainerObject.java
@Cube(“tomme”)
@Image("tomee:8-jre-1.7.2-webprofile")
public static class ImageContainerObject {
}

So in this case Arquillian Cube starts and stops the image defined in the annotation.

Arquillian Standalone and Cube

You can use Arquillian Standalone with Arquillian Cube too. Arquillian Standalone is a mode of Arquillian which allows you to use Arquillian but without deploying any application. Basically it means no @Deployment static method and tests runs as client implicitly.

Running Arquillian Cube in Standalone mode means that Arquillian Cube starts all defined containers in the correct order. Internally Arquillian Cube registers a autostartContainers`property (in case you have not registered any) with `regexp:.* expression.

Dependencies you need to set for Standalone mode are:

pom.xml
<dependencies>
    <dependency>
        <groupId>org.jboss.arquillian.junit</groupId>
        <artifactId>arquillian-junit-standalone</artifactId> <!-- 1 -->
        <scope>test</scope>
    </dependency>
    <dependency>
         <groupId>org.arquillian.cube</groupId>
         <artifactId>arquillian-cube-docker</artifactId>
         <scope>test</scope>
    </dependency>
</dependencies>
  1. You need to change arquillian-junit-container to standalone.

Notice that in case of Standalone mode, Arquillian Cube registers automatically autostartContainers property to regexp:.*. If you want to avoid this, you can set yourself this property to [none]. Then Arquilian Cube will not touch anything and you will be the responsible of starting manually each instance using for example CubeController class by your own.

Containerless Server and Docker

In all previous sections we have seen that the application is deployed inside a container. For example in case of Tomcat application, resources are deployed inside a Servlet container or for example in case of Apache TomEE you can deploy EJBs inside an EJB container.

But nowadays there other kind of applications that contains the container (if they have one) embedded inside them. Typically these applications uses an embedded server and they are run as CLI applications. Some examples can be Spring Boot, Netty, SparkJava or Undertow.

If you are using some of these technologies with Docker, you can still use Arquillian Cube to write your tests.

Java Embedded Servers

Let’s suppose we are writing a service which should return as text the current day and time. To serve this service to the world we decide to use undertow embedded server.

The code looks like:

DaytimeServer.java
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DaytimeServer {

  public static void main(String[] args) { //(1)

      Undertow server = Undertow.builder()
              .addHttpListener(8080, "0.0.0.0")
              .setHandler(new HttpHandler() {
                @Override
                public void handleRequest(final HttpServerExchange exchange) throws Exception {
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
                    exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
                    exchange.getResponseSender().send(simpleDateFormat.format(new Date()) + System.lineSeparator()); //(2)
                }
            }).build();
       server.start();
  }
}
  1. This class is a CLI application.

  2. Returns a text with the day and time formatted with SimpleDateFormat.

See that this application is a CLI application which is pretty different from previous examples. Previously the packaged application was deployed inside an application server, which in fact means that Arquillian connects to the server and tells it to deploy that file.

In this example there is no application server nor servlet server waiting for Arquillian to deploy an archive but the application is self-contained, it contains everything. So in fact if you want to run the application probably you will end up by doing something like java -jar daytime.jar.

So how to write a test for these classes if we are using Docker as runtime container?

The first thing to do is add arquillian-cube-containerless dependency.

pom.xml
<dependency>
  <groupId>org.arquillian.cube</groupId>
  <artifactId>arquillian-cube-containerless</artifactId>
  <version>${arquillian.cube.version}</version>
</dependency>

Next step is creating a Dockerfile. This is required because we need to set not only the container image to be used but how to run the application. But see that there is a problem on creating a Dockerfile in this case. The jar name is not static because it will depend on the name you give during the creation of the archive (using Shrinkwrap). So in fact Dockerfile should be templaterized. And this is something that Arquillian Cube can do for you. The idea is creating a file called DockerfileTemplate.

src/test/resources/daytime/DockerfileTemplate
FROM java:7

WORKDIR /usr/src/server
COPY ${deployableFilename} /usr/src/server/${deployableFilename} #1
EXPOSE 8080
CMD ["java", "-jar", "${deployableFilename}"]
  1. ${deployableFilname} will be replaced at runtime by the name of the jar file created by Shrinkwrap.

Then we need to touch arquillian.xml file by setting an special container definition so Arquillian doesn’t crash because of trying to deploy the archive into a none defined container.

src/test/resources/arquillian.xml
<?xml version="1.0"?>
<arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://jboss.org/schema/arquillian"
  xsi:schemaLocation="http://jboss.org/schema/arquillian
  http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

  <extension qualifier="docker">
    <property name="serverVersion">1.12</property>
    <property name="serverUri">http://localhost:2375</property>
    <property name="dockerContainers"> <!--(1)-->
      daytime:
        buildImage: <!--(2)-->
          dockerfileLocation: src/test/resources/undertow <!--(3)-->
          noCache: true
          remove: true
        await:
          strategy: polling
        portBindings: [8080/tcp]
    </property>
  </extension>

  <container qualifier="containerless" default="true"> <!--(4)-->
    <configuration>
        <property name="containerlessDocker">daytime</property> <!--(5)-->
        <property name="embeddedPort">8080</property> <!--(6)-->
    </configuration>
  </container>

</arquillian>
  1. The Docker container is defined as per usual.

  2. buildImage attribute is used to define the dockerfile location.

  3. This attribute sets the directory where the Dockerfile is stored. In fact in this case it is the directory where DockerfileTemplate file is stored.

  4. A container provided by Arquillian Cube must be defined.

  5. This property is used to set which container must be started.

  6. This property sets the exposed port by the embedded server.

Important
If containerless definition only contains only one image, it is not necessary to use containerlessDocker property. At the same time if the image only exposes one port, it is not necessary to use embeddedPort proeprty to set the port. So in previous example you could avoid using containerlessDocker and embeddedPort.

And finally the test:

DaytimeTest.java
@RunWith(Arquillian.class)
public class DaytimeTest {

  private static final String LINE_SEPARATOR = System
          .getProperty("line.separator");

  @Deployment(testable = false) //(1)
  public static JavaArchive createDeployment() {
      JavaArchive[] undertow = Maven.resolver().resolve("io.undertow:undertow-core:1.1.1.Final").withTransitivity().as(JavaArchive.class); //(2)

      JavaArchive jar = ShrinkWrap
              .create(JavaArchive.class, "daytime.jar")
              .addClass(DaytimeServer.class); //(3)

      for (JavaArchive javaArchive : undertow) { //(4)
          jar.merge(javaArchive);
      }

      jar.addAsManifestResource(
              new StringAsset(
                      "Main-Class: org.arquillian.cube.impl.containerless.DaytimeServer"
                              + LINE_SEPARATOR), "MANIFEST.MF"); //(5)
      return jar;
  }

  @Test
  public void shouldReturnDateFromDaytimeServer(@ArquillianResource URL base) { //(6)
      try (
              BufferedReader in = new BufferedReader(new InputStreamReader(
                    base.openStream()));) {
          String userInput = in.readLine();
          assertThat(userInput, notNullValue());
      } catch (UnknownHostException e) {
          fail("Don't know about host ");
      } catch (IOException e) {
          fail("Couldn't get I/O for the connection to ");
      }
  }
}
  1. Tests should be run as-client.

  2. ShrinkWrap Maven resolver gets all dependencies for Undertow.

  3. Create a jar file called daytime.jar with DaytimeServer class.

  4. Undertow dependencies are merged inside jar.

  5. Because it is a runnable jar, MANIFEST is created accordantly.

  6. Simple test.

Polyglot Applications

In previous section we have seen that we can test any java CLI application that offers a socket connection. But if you think clearly there is nothing that avoid Arquillian Cube to deploy applications developed in other languages like Node.js, Play, Ruby on Rails, …​

Let’s see an example on how you can use Arquillian Cube to test a Node.js hello world application.

First thing to do is create the Node.js application.

src/main/js/package.json
{
  "name": "helloworld-server",
  "version": "0.0.1",
  "description": "A NodeJS webserver to run inside a docker container",
  "author": "asotobu@gmail.com",
  "license": "APLv2",
  "dependencies": {
      "express": "*"
  },
  "scripts": {"start": "node index.js"}
}
src/main/js/index.js
var express = require('express');

var app = express();

app.get('/', function(req, res){
  res.send('Hello from inside a container!');
});

app.listen(8080);

Then we need to define a DockerfileTemplate as we did for Undertow.

src/test/resources/node/DockerfileTemplate
FROM node:0.11.14

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

ADD ${deployableFilename} /usr/src/app #1
RUN npm install
EXPOSE 8080

CMD [ "npm", "start" ]
  1. We need to use ADD command adding the deployed file instead of COPY. We are going to see why below.

Finally the arquillian.xml configuration file.

arquillian.xml
<?xml version="1.0"?>
<arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://jboss.org/schema/arquillian"
  xsi:schemaLocation="http://jboss.org/schema/arquillian
  http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

  <extension qualifier="docker">
    <property name="serverVersion">1.12</property>
    <property name="serverUri">http://localhost:2375</property>
    <property name="dockerContainers">
        node:
          buildImage:
            dockerfileLocation: src/test/resources/node
            noCache: true
            remove: true
          await:
            strategy: polling
          portBindings: [8080/tcp]
    </property>
  </extension>

  <container qualifier="containerless" default="true">
    <configuration>
      <property name="containerlessDocker">node</property> <!--(1)-->
      <property name="embeddedPort">8080</property>
    </configuration>
  </container>

</arquillian>
  1. This property is used to set which container must be started. In this case node.

Important
If containerless definition only contains only one image, it is not necessary to use containerlessDocker property. At the same time if the image only exposes one port, it is not necessary to use embeddedPort proeprty to set the port. So in previous example you could avoid using containerlessDocker and embeddedPort.

And finally the Arquillian test.

NodeTest.java
@RunWith(Arquillian.class)
public class NodeTest {

  @Deployment(testable = false) //(1)
  public static GenericArchive createDeployment() {
    return ShrinkWrap.create(GenericArchive.class, "app.tar") //(2)
            .add(new ClassLoaderAsset("index.js"), "index.js")
            .add(new ClassLoaderAsset("package.json"), "package.json");
  }

  @Test
  public void shouldReturnMessageFromNodeJs(@ArquillianResource URL base) { //(3)
    try (BufferedReader in = new BufferedReader(new InputStreamReader(
            base.openStream()));) {
        String userInput = in.readLine();
        assertThat(userInput, is("Hello from inside a container!"));
    } catch (UnknownHostException e) {
        fail("Don't know about host ");
    } catch (IOException e) {
        fail("Couldn't get I/O for the connection to ");
    }
  }
}
  1. Tests should be run as-client.

  2. GenericArchive with tar extension must be created using Shrinkwrap.

  3. Simple test.

Note
GenericArchive must end with tar extension because it is expected by Arquillian Cube. When you use ADD in Dockerfile, Docker will untar automatically the file to given location.

Future work

API will continuously evolve to fit requirements of an enterprise application as well as providing integration with Kubernates and other Docker related tools. Also some configuration parameters will be modified to fix any possible requirements. Although we are going to try to not break compatibility with previous versions, we cannot guarantee until beta stage.

Feel free to use it and any missing feature, bug or anything you see , feel free to add a new issue.

About

Arquillian Docker

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Java 100.0%