Jenkins CI Pipeline with Ruby

Building a Jenkins CI Pipeline with Sinatra + Test::Unit

This article introduction to support testing Ruby web application using Jenkins. We will create a Jenkins pipeline (using Jenkinsfile) to build and test the application. During this process, I’ll demonstrate how to build a small HelloWorld API with Sinatra, a popular web microframework originally released in 2007, how to create some xUnit style unit tests for the service, and then how to integrate this into Jenkins.

This is a minimal material to get started, so we will only have a build and test stage. In production, we would actually want to have a third stage called push, to conditionally push an artifact, the output of the CI, to an artifact repository, such as RubyGems site for a gem, or a Docker image to a Docker registry.

Part 1: The Web Application

The web application is a simple Hello World web application with essentially three routes: / , /hello, and /hello/<name>, where name is any name you desire. The Sinatra library interface uses an API that matches the HTTP protocol, such as GET, PUT, POST. So, this makes it crazy simple to build a quick website with little effort.

Get Ruby

First we need to get a ruby 2.3.1 or greater. I highly recommend using a ruby version manager like RVM or rbenv. I wrote some previous articles on this topic:

RVM

rbenv

After getting a ruby version is installed (current version is 2.6.1), we need to install Ruby gem packages. We can create a package manifest called a Gemfile and then install the packages with Bundler using these bash commands:

# install bundler 
gem
install bundler
# create package manifest
cat <<-'PACKAGE_MANIFEST' > Gemfile
source "https://rubygems.org"

gem 'sinatra'
gem 'sinatra-contrib'

group :test do
gem 'rack-test'
gem 'ci_reporter_test_unit'
end
PACKAGE_MANIFEST
# install packages
bundle
install

The Application

For the application create a file called app.rb with the following contents:

#!/usr/bin/env ruby
# app.rb
require 'sinatra'
require "sinatra/multi_route"

# Override Defaults
set :port, 3000 # WEBrick=4567,
set :bind, '0.0.0.0' # WEBrick=localhost

class HelloWorldApp < Sinatra::Base
register Sinatra::MultiRoute
  get '/', '/hello', '/hello/' do
"Hello, world!\n"
end

get '/hello/:name' do
"Why Hello #{params[:name]}!\n"
end
end

You can try the server out with ruby app.rb or with:

# make script executable & run service
chmod
+x app.rb
./app.rb &
# test the server 
curl
-i localhost:3000/
curl -i localhost:3000/hello
curl -i localhost:3000/hello/Simon

The Middleware

One popular tool for running a ruby web application is to use Rack. Rack is a middleware layer between the a web server and ruby frameworks. The web server we are using is a small developer web server called WebBrick. The framework we are using is Sinatra.

Sinatra itself can automatically bootstrap itself using WebBrick, but with Rack, we can use another solution like Unicorn, Puma, or Passenger. To keep things simple, we’ll still use WebBrick, but control it through Rack.

Create a config.ru file with these bash commands:

cat <<-RACK_CONFIG > config.ru
# config.ru
require 'rubygems'
require 'bundler'

Bundler.require

require './app'
#\ -w -p 3000 --host 0.0.0.0 # Override default Rack port 9292
run HelloWorldApp
RACK_CONFIG

Once our configuration is in place, we can start it up with the following:

# start the service through rack
rackup &
# test the server 
curl
-i localhost:3000/
curl -i localhost:3000/hello
curl -i localhost:3000/hello/Simon

Part 2: The Unit Tests

Before we tested the application with three routes: /, /hello, and /hello/Simon. Now we can write some tests to test these routes.

Create the Tests

Run these in bash to create our test cases:

mkdir -p test
cat <<-'TEST_CASES' > test/app_test.rb
#!/usr/bin/env ruby
ENV['RACK_ENV'] = 'test'

require_relative '../app'
require 'test/unit'
require 'rack/test'

set :environment, :test

class AppTest < Test::Unit::TestCase
include Rack::Test::Methods

def app
# retreive class name containing Sinatra app
Rack::Builder.parse_file("config.ru").first
end

def test_it_says_hello_world_root
get '/'
assert last_response.ok?
assert_equal "Hello, world!\n", last_response.body
end

def test_it_says_hello_world_w_hello
get '/hello'
assert last_response.ok?
assert_equal "Hello, world!\n", last_response.body
end

def test_it_says_hello_to_a_person
name = "Simon"
get "hello/#{name}"
assert last_response.ok?
assert last_response.body.include?(name)
end
end
TEST_CASES
chmod +x test/app_test.rb

Code Walkthrough

We use two libraries, the test framework called Test Unit based on xUnit principles and Rack Test, a small testing API for Rack apps.

To get started with Test Unit, we put all of our tests in a class derived from Test::Unit::TestCase. Each test is a method within this class that begins with the name test_.

We need to define a method app that returns an instances of our class. We can leverage from the Rack configuration, config.ru, to fetch the name of our class HelloWorldApp. Then we create methods that represent each of our tests for routes: /, /hello, and /hello/name. We will call the get method, and then do asserts on last_response.

Running the Tests

To run the tests, we simply run something like:

./test/app_test.rb -- verbose

We’ll get some output like this:

Loaded suite ./test/app_test
Started
AppTest:
test_it_says_hello_to_a_person: .: (0.215978)
test_it_says_hello_world_root: .: (0.003210)
test_it_says_hello_world_w_hello: .: (0.002533)

Finished in 0.222281 seconds.
--------------------------------------------------------------------
3 tests, 6 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
--------------------------------------------------------------------
13.50 tests/s, 26.99 assertions/s

Task Runner

Ruby community has a powerful task tool, called Rake. We can use this as a wrapper for our tests. We’ll need to install the rake tool, and create a Rakefile. Then we can run our tests by typing rake.

# install rake
gem install Rake
# create our test task
cat <<-'RAKEFILE' > Rakefile
require 'rake/testtask'

task default: %i(test)

Rake::TestTask.new do |t|
t.pattern = 'test/*_test.rb'
t.warning = false
t.verbose = true
end
RAKEFILE
# run the tests
rake

Part 3: The Jenkins Pipeline

Now that we have our web application, unit tests, and some automation with Rack and Rake, we can create a Jenkins CI Pipeline by creating a Jenkinsfile. The Jenkinsfile is a Groovy script, and can use a DSL-like syntax to define our stages and shell instructions.

The Jenkinsfile

We’ll have three stages: requirements, build, and test for our current pipeline. Use this bash command to create the Jenkinsfile:

cat <<-'JENKINSFILE' > Jenkinsfile
pipeline {
agent { docker { image 'ruby:2.6.1' } }
stages {
stage('requirements') {
steps {
sh 'gem install bundler -v 2.0.1'
}
}
stage('build') {
steps {
sh 'bundle install'
}
}
stage('test') {
steps {
sh 'rake'
}
}
}
}
JENKINSFILE

When this is used by a Jenkins agent, it will download a Docker image with Ruby and Rake already installed, and then we add Bundler in our requirements stage. For build and test stagers, the pipeline will run a shell command similar to have we have already ran in our previous steps.

Getting a Jenkins Server

Jenkins has docker image that contains everything we need for this project. We can run this container for all of our Jenkins needs. I have a tutorial on running this locally in your development system, as long as you have Docker installed.

Import the Project

After logging in to your Jenkins servers, you’ll want to import a pipeline. This code will have to be checked into a Git repository (or other Source Code Manager), and then configured to fetch the Jenkinsfile from that repository.

I have a small project you can use with the code for this repository:

Steps to Import

To import a new project:

  1. Create a New Item, and select Pipeline.
  2. Select Pipeline tab
  3. In Definition field, select Pipeline Script from SCM
  4. In SCM field, select Git
  5. In Repository URL, paste a git URL

For the git URL, you can an we URL (https) for this tutorial, but when using professionally, you’ll want to use an SSH URL and manage credentials with Jenkins Credentials Plug-In.

The configuration should look something like this:

Jenkins 2.x UI for Pipeline Configuration

Running the Pipeline

Once configured, click on the Open Blue Ocean link. For the first time, you’ll be prompted to Run this pipeline, click the Run button. You’ll have a new item, click on that, and you’ll see an information similar to the one below, where you can expand the stages to see live action play of the commands as they are running:

Jenkins Blue Oceans UI running Pipeline

Test Report Integration

Jenkins has the ability present test results in a graphical visual way, as long as you can output the results in a JUnit format. JUnit is a popular xUnit type of test framework, and JUnit output format (an XML file) is sort of a standard for test reporting. Essentially, any CI (Continuous Integration) solution will support this format, including Jenkins.

For this integration, we can add the support in our task automation script or Rakefile. Update the Rakefile to include this below:

require 'rake/testtask'
require 'ci/reporter/rake/test_unit'

task default: %i(test)
task testunit: 'ci:setup:testunit'

namespace :ci do
task :all => ['ci:setup:testunit', 'test']
end

Rake::TestTask.new do |t|
t.pattern = 'test/*_test.rb'
t.warning = false
t.verbose = true
end

With this setup, running rake ci:all will generate the JUnit report the ./test/reports directory. We will need to tell Jenkins where this is located. Update Jenkinsfile with the following below:

pipeline {
agent { docker { image 'ruby:2.6.1' } }
stages {
stage('requirements') {
steps {
sh 'gem install bundler -v 2.0.1'
}
}
stage('build') {
steps {
sh 'bundle install'
}
}
stage('test') {
steps {
sh 'rake ci:all'
}
post {
always {
junit 'test/reports/TEST-AppTest.xml'
}
}
}
}
}

We essentially modified the test stage to use our all task in the ci namespace. This will generate the JUnit report. At the end of this test stage, we’ll always run a post step whether the tests pass or fail. In this post step, we’ll tell Jenkins where to fetch the JUnit Report.

After checking in this and merging it to your repository, you can re-run the job to see the changes. If you used my repository, the changes will already be in there. To see the results, just lick on the Tests tab in the BlueOcean interface.

Jenkins Blue Ocean UI for JUnit Test Report

Wrapping Up

In this tutorial, we covered how to:

  • build a small web API server using Sinatra with Rack middleware.
  • tool in xUnit style unit tests with automation from Rake task tool.
  • create a Jenkins pipeline with JUnit test report integration.

To take this from an introductory tutorial and apply it to a professional implementation, we would want to add the following to our pipeline:

  • a commit to a master (or release) branch trigger this pipeline with a step that releases a package to an artifact repository, but only if the tests pass.
  • a pull or merge request would trigger this pipeline, and the artifact would be tests results published in a git service like GitHub. A test failure, would block that branch from being merged to master or release branch.

For the first step, you would need to create credentials in Jenkins (using a credentials plug-in) and then reference these credentials in the Jenkisnfile. The credentials would grant access to the git repository and to an artifact repositories like RubyGems, Docker Hub, Quay, Nexus, or Artifactory.

For git server integration with something like GitHub, we would need to use a webhook for visual feedback to GitHub. Some links provided below for further information.

I hope this was useful. Happy hacking.

Resources

GitHub Integration

Artifact Repositories

Web-App Servers

Miscellaneous