Image for post
Image for post

Jenkins CI Pipeline with Ruby

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

This article introduction to support testing 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 , a popular web microframework originally released in 2007, how to create some 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 site for a gem, or a image to a 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 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 or . 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 gem packages. We can create a package manifest called a Gemfile and then install the packages with 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 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 . The framework we are using is .

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

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 based on principles and , a small testing API for apps.

To get started with , 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 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

community has a powerful task tool, called . 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 and , we can create a CI Pipeline by creating a Jenkinsfile. The Jenkinsfile is a script, and can use a -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 agent, it will download a image with and already installed, and then we add 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

has docker image that contains everything we need for this project. We can run this container for all of our needs. I have a tutorial on running this locally in your development system, as long as you have 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 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 .

The configuration should look something like this:

Image for post
Image for post
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:

Image for post
Image for post
Jenkins Blue Oceans UI running Pipeline

Test Report Integration

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

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 report the ./test/reports directory. We will need to tell 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 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 where to fetch the 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 interface.

Image for post
Image for post
Jenkins Blue Ocean UI for JUnit Test Report

Wrapping Up

In this tutorial, we covered how to:

  • build a small web API server using with middleware.
  • tool in style unit tests with automation from task tool.
  • create a pipeline with 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 (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 , , , , or .

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

I hope this was useful. Happy hacking.

Resources

GitHub Integration

Artifact Repositories

Web-App Servers

Miscellaneous

Written by

Linux NinjaPants Automation Engineering Mutant — exploring DevOps, Kubernetes, CNI, IAC

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store