HowTo apply Test Driven Development to Container Images

Apply Test Driven Development(TDD) to the process of building container images by defining test before writing code and automate the testing process. Iterate through the TDD cycle while developing and running the tests later in continuous integration to ensure robust and reliable container images.

Installation

We create a Gemfile for installing all required gems.

# Gemfile
gem 'docker-api'
gem 'serverspec'
gem 'rspec'

Then we install them

bundle install

Preparation

Create the directory where the Tests are going to live. Add the folder to .dockerignore since we wo not want the test to be included within our final container image.

mkdir spec
echo spec >> .dockerignore

spec/spec_helper.rb

require the used gems

require 'serverspec'
require 'docker'

Build the container image from the project's root folder

image = Docker::Image.build_from_dir('.')

Setup serverspec to run the tests against a container based on the previous build image

set :backend, :docker
set :docker_image, image.id

The test driven workflow

create the Dockerfile

Boilerplate Dockerfile to ensure the image building process is working

# Dockerfile
FROM debian:stable

Write first test

We want to have a script file /entrypoint.sh in our image

describe file '/entrypoint.sh' do
  it { should exist }
end

See the test failing

Run the test to see it fail

bundle exec rspec
F

Failures:

  1) File "/entrypoint.sh" is expected to exist
     Failure/Error: it { should exist}
       expected File "/entrypoint.sh" to exist

     # ./spec/dockerfile_spec.rb:10:in `block (2 levels) in <top (required)>'

Finished in 1.94 seconds (files took 0.31723 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/dockerfile_spec.rb:10 # File "/entrypoint.sh" is expected to exist

Fix the test

entrypoint.sh

#!/bin/sh
# Dockerfile
FROM debian:stable
COPY entrypoint.sh / 

Run the test again

bundle exec rspec
.

Finished in 1.78 seconds (files took 0.33197 seconds to load)
1 example, 0 failures

run the test inside a GitLab CI Job

Assumed the image was build and pushed to the container registry in a job that has already finished, we do not need to build the image from scratch but pull it from the container registry

update spec_helper

Determine if the tests are running inside GitLab CI. If yes, authenticate against the container registry and pull the image. Otherwise build the image locally


# check if this is a gitlab ci pipeline
def in_gitlab_ci?
  ENV['CI'].eql?('true')
end

if in_gitlab_ci?
  Docker.authenticate!(
    username:      ENV['CI_REGISTRY_USER'],
    password:      ENV['CI_REGISTRY_PASSWORD'],
    serveraddress: ENV['CI_REGISTRY']
  )
  Docker::Image.create(
    fromImage: ENV['CI_REGISTRY_IMAGE'],
    tag: ENV['IMAGE_TAG'] || 'latest',
  )
else
  image = Docker::Image.build_from_dir('.')
end

GitLab Job definition

Create a job that runs on a GitLab CI shell runner with Docker installed locally. Install the required Gems and cache them for subsequent job runs. Run the tests and pass the results to GitLab in the JUnit fromat for displaying it in the GUI.

test container image:
  artifacts:
    reports:
      junit: serverspec-junit.xml
  tags:
    - shell_runner
  cache:
    key: Gemfile.lock
    paths:
      - vendor/ruby
  script:
    - bundle config set --local path 'vendor/ruby'
    - bundle install
    - bundle exec rspec spec/*_spec.rb
      --format documentation
      --format RspecJunitFormatter
      --out serverspec-junit.xml

References

Moritz Kraus 4 months ago