Leibniz is an integration testing framework for Chef
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
bin
features
lib
templates
.gitignore
.travis.yml
Gemfile
LICENSE.txt
README.md
Rakefile
TODO.md
WISHLIST.md
leibniz.gemspec

README.md

Code Climate

Leibniz

Leibniz is simple utility which provides the ability to launch infrastructure using Test Kitchen, and run acceptance tests against that infrastructure. It is designed to be used as part of a set of Cucumber / Gherkin features.

Installation

Add this line to your application's Gemfile:

gem 'leibniz'

And then execute:

$ bundle

Or install it yourself as:

$ gem install leibniz

Usage

The Leibniz CLI

Since version 0.2.0, Leibniz provides a basic CLI which will create and populate an example Cucumber feature to get you started. You can call this using:

leibniz init

Getting Started

Leibniz takes the view that acceptance testing of infrastructure should be performed from the outside in, and make assertions from the perspective of an external user consuming the delivered infrastructure.

To get started, you will need to write some features and some steps. Depending on how you build your infrastructure (at present the assumed approach is Berkshelf and wrapper cookbooks, but there's no reason why it wouldn't work with Librarian-chef, or some other approach).

Using Berkshelf

Assuming you have Berkshelf installed, you can use the in-built cookbook generator to create your wrapper cookbook. The alternative is to create a cookbook directory, or use knife to create a cookbook, and then add 'berkshelf' to a Gemfile, and run bundle install, followed by berks init.

Either way, once you have a cookbook which has been 'berksified' you will have something that looks like this:

.
├── Berksfile
├── Gemfile
├── LICENSE
├── README.md
├── Thorfile
├── Vagrantfile
├── attributes
├── chefignore
├── definitions
├── files
│   └── default
├── libraries
├── metadata.rb
├── providers
├── recipes
│   └── default.rb
├── resources
└── templates
    └── default

Writing your first feature

We are going to take you through the process of iterating on the creation of a feature and its step definitions. In reality you might merge some of these steps together and not run cucumber as often as we do here, but this illustrates every step in the process.

First we need to create a directory to contain our features, then write our first feature:

mkdir features
cd features
vi generic_webpage.feature

The feature you write needs to have a Background section like this:

Background:

  Given I have provisioned the following infrastructure:
  | Server Name     | Operating System | Version | Chef Version | Run List                 |
  | generic_webpage | ubuntu           |   12.04 |       11.8.0 | generic_webpage::default |
  And I have run Chef

This background section is responsible for provisioning infrastructure and getting it into a state whereupon we can run some acceptance tests against it. The title of each column is defined by Leibiniz:

  • Server Name - this is the name of the machine you will be provisioning. Leibniz will prepend leibniz to the name and will launch a machine with this name.
  • Operating System - this translates to the base OS of a Vagrant box which is downloaded on demand. The boxes used are Opscode's 'Bento' boxes, and have nothing other than a base OS installed. At present ubuntu, debian, centos and fedora are supported.
  • Version - this is version of the Operating System. See the Bento website for an up-to-date specification of the available versions.
  • Chef Version - this is the version of the Chef 'client' software to be installed.
  • Run List - this is the Chef run list which will be used when the node is converged.

These two steps are satisfied by Leibniz. We need to ensure the Leibniz library is available to Chef. To do this, add leibniz to your Gemfile. We will also be needing rspec-expectations so this should be added to your Gemfile aswell. Now, run bundle install. Finally, create a support directory under your features directory, and within the support directory, create an env.rb file. This should read:

require 'leibniz'
require 'rspec/expectations'
World(RSpec::Matchers)

Now create your step definitions:

mkdir features/step_definitions
vi features/step_definitions/generic_webpage_steps.rb

The following steps will build and converge the infrastructure described in the table:

Given(/^I have provisioned the following infrastructure:$/) do |specification|
  @infrastructure = Leibniz.build(specification)
end

Given(/^I have run Chef$/) do
  @infrastructure.destroy
  @infrastructure.converge
end

At present, Leibniz only knows how to provision infrastructure using the Vagrant driver. By default this will assume you have Virtualbox on the system where you are running Cucumber. A top priority is to support other Kitchen drivers, which will enable infrastructure to be provisioned on cloud platforms, via LXC or Docker, or just with Vagrant.

Once you have your feature, env.rb and steps in place, you can run cucumber. This will build the infrastructure you described using Chef.

You may find it useful to tail the logs during this process:

tail -f .kitchen/logs/leibniz-generic-webpage.log

If all goes well, you should see something like:

Feature: Serve a generic webpage

  In order to demonstrate how Leibniz works
  As an infrastructure developer
  I want to be able to serve a generic webpage and test it

  Background:                                              # features/crap_webpage.feature:7
    Given I have provisioned the following infrastructure: # features/step_definitions/generic_webpage_steps.rb:1
      | Server Name     | Operating System | Version | Chef Version | Run List                 |
      | generic_webpage | ubuntu           | 12.04   | 11.8.0       | generic_webpage::default |
Using generic_webpage (0.1.0) from metadata
    And I have run Chef                                    # features/step_definitions/generic_webpage_steps.rb:5

0 scenarios
2 steps (2 passed)
0m58.613s

At this stage we have only provisioned the machine per the table we provided in the feature. We now need to describe an example of what the infrastructure does. Open the feature and add an example:

  Scenario: Infrastructure developer can see generic webpage
    Given a URL "http://generic-webpage.com"
    When I browse to the URL
    Then I should see "This is a generic webpage"

When we run cucumber again, we should be told that our steps are undefined. Cucumber will suggest some snippets we can use:

You can implement step definitions for undefined steps with these snippets:

Given(/^a URL "(.*?)"$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

When(/^I browse to the URL$/) do
  pending # express the regexp above with the code you wish you had
end

Then(/^I should see "(.*?)"$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

Copy and paste these into features/step_definitions/generic_webpage_steps.rb, then run cucumber again. We should now see that the step runs, but is marked as pending - that is we haven't implemented the acceptance test.

The idea of Leibniz is to make the provisioning and converging of infrastructure nodes as painless and flexible as possible, enabling the infrastructure developer to dive into writing the acceptance tests right away. Future versions of the library may ship some useful features to help with common acceptance test types.

We now need to write the implementation of our acceptance tests:

The first step simply requires us to capture a host header that we can use as part of an http client:

Given(/^a URL "(.*?)"$/) do |url|
  @host_header = url.split("/").last
end

The second step requires us to use an http client. There are many options available for this, Faraday is a simple one:

When(/^I browse to the URL$/) do
  connection = Faraday.new(url: "http://#{@infrastructure['generic-webpage'].ip}", headers: { 'Host' => @host_header }) do |faraday|
    faraday.adapter Faraday.default_adapter
  end
  @page = connection.get('/').body
end

Note: the ip of the infrastructure we have built is available as part of the @infrastructure object returned by Leibniz.

The final step is a simple rspec expectation:

Then(/^I should see "(.*?)"$/) do |content|
  expect(@page).to match /#{content}/
end

When we run cucumber this time, the tests will fail because we've not implemented anything to actually serve a website, nor have we deployed the website for the web server to serve.

Making the tests pass (i.e. writing the Chef code to deploy a web server and serve a static web page) is left as an exercise for the reader. One the code is written, running cucumber again will converge the node and run the acceptance tests, resulting in the tests passing, like this:

Feature: Serve a generic webpage

  In order to demonstrate how Leibniz works
  As an infrastructure developer
  I want to be able to serve a generic webpage and test it

  Background:                                              # features/generic_webpage.feature:7
    Given I have provisioned the following infrastructure: # features/step_definitions/generic_webpage_steps.rb:3
      | Server Name     | Operating System | Version | Chef Version | Run List                 |
      | generic_webpage | ubuntu           | 12.04   | 11.8.0       | generic_webpage::default |
Using generic_webpage (0.1.0)
Using apt (2.3.0)
Using apache2 (1.8.4)
    And I have run Chef                                    # features/step_definitions/generic_webpage_steps.rb:7

  Scenario: Infrastructure developer can see generic webpage # features/generic_webpage.feature:14
    Given a URL "http://generic-webpage.com"                 # features/step_definitions/generic_webpage_steps.rb:12
    When I browse to the URL                                 # features/step_definitions/generic_webpage_steps.rb:16
    Then I should see "This is a generic webpage"            # features/step_definitions/generic_webpage_steps.rb:23

1 scenario (1 passed)
5 steps (5 passed)
1m25.227s

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request