├── .travis.yml ├── templates ├── features │ ├── support │ │ └── env.rb │ ├── step_definitions │ │ └── learning_steps.rb │ └── learning_leibniz.feature └── .leibniz.yml.erb ├── lib ├── leibniz │ └── version.rb └── leibniz.rb ├── features ├── support │ └── env.rb ├── step_definitions │ └── leibniz_init_steps.rb ├── leibniz_command.feature └── leibniz_init.feature ├── Rakefile ├── Gemfile ├── .gitignore ├── TODO.md ├── LICENSE.txt ├── leibniz.gemspec ├── bin └── leibniz ├── WISHLIST.md └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | -------------------------------------------------------------------------------- /templates/features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'leibniz' 2 | -------------------------------------------------------------------------------- /lib/leibniz/version.rb: -------------------------------------------------------------------------------- 1 | module Leibniz 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'aruba/cucumber' 2 | require 'leibniz' 3 | 4 | Before do 5 | @aruba_timeout_seconds = 15 6 | end 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'cucumber/rake/task' 3 | 4 | Cucumber::Rake::Task.new 5 | 6 | task :default => [:cucumber] 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | #ruby=ruby-2.0.0 4 | #ruby-gemset=leibniz 5 | 6 | # Specify your gem's dependencies in leibniz.gemspec 7 | gemspec 8 | -------------------------------------------------------------------------------- /templates/.leibniz.yml.erb: -------------------------------------------------------------------------------- 1 | --- 2 | driver: <%= config[:driver] %> 3 | network: 10.2.3.0/24 4 | suites: 5 | - name: leibniz 6 | run_list: [] 7 | data_bags_path: "test/integration/default/data_bags" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .idea/ 19 | -------------------------------------------------------------------------------- /templates/features/step_definitions/learning_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^I have provisioned the following infrastructure:$/) do |specification| 2 | @infrastructure = Leibniz.build(specification) 3 | end 4 | 5 | Given(/^I have run Chef$/) do 6 | @infrastructure.destroy 7 | @infrastructure.converge 8 | end 9 | -------------------------------------------------------------------------------- /features/step_definitions/leibniz_init_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^I have a wrapper cookbook called "(.*?)"$/) do |cookbook| 2 | step "I successfully run `knife cookbook create #{cookbook} -o .`" 3 | 4 | end 5 | 6 | Given(/^I elect to use the "(.*?)" driver$/) do |driver| 7 | step "I successfully run `leibniz init --driver dummy`" 8 | end 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/features/learning_leibniz.feature: -------------------------------------------------------------------------------- 1 | Feature: Learn to use Leibniz 2 | 3 | In order to learn how to use Leibniz 4 | As an infrastructure developer 5 | I want to be able to have a skeleton feature 6 | 7 | Background: 8 | 9 | Given I have provisioned the following infrastructure: 10 | | Server Name | Operating System | Version | Chef Version | Run List | 11 | | learning | centos | 6.4 | 11.8.0 | learning::default | 12 | And I have run Chef 13 | 14 | Scenario: Infrastructure developer can learn Leibniz 15 | pending 16 | # Write your features here! -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Things to do on Leibniz 2 | 3 | - High level documentation 4 | - Remove some nasty magic numbers 5 | - On a physical box, prove that we can run a Leibniz job from Jenkins 6 | - Try to get Leibniz to with TK to with the EC2 driver (or LXC / Docker) 7 | - Demonstrate that this can work with Librarian? 8 | - Demonstrate (and document) how to make Leibniz use the chef-zero provisioner 9 | - Provide a generator which creates the outline stuff 10 | - It'd be nice to be able to run the cucumber test and skip the teardown and setup process 11 | - It'd be ace if we could not have to install chef each time (but that might mean a backed image) -------------------------------------------------------------------------------- /features/leibniz_command.feature: -------------------------------------------------------------------------------- 1 | Feature: A command line interface for Leibniz 2 | In order to simplify the process of getting started with Leibniz Acceptance Testing 3 | As a Leibniz user 4 | I want a command line interface that has sane defaults and built in help 5 | 6 | Scenario: Displaying help 7 | When I run `leibniz help` 8 | Then the exit status should be 0 9 | And the output should contain "leibniz init" 10 | And the output should contain "leibniz version" 11 | 12 | Scenario: Displaying the version of Leibniz 13 | When I run `leibniz version` 14 | Then the exit status should be 0 15 | And the output should contain "Leibniz version" 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Atalanta Systems Ltd 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /leibniz.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'leibniz/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "leibniz" 8 | spec.version = Leibniz::VERSION 9 | spec.authors = ["Stephen Nelson-Smith"] 10 | spec.email = ["stephen@atalanta-systems.com"] 11 | spec.description = %q{Automated Infrastructure Acceptance Tests} 12 | spec.summary = %q{Arguably Leibniz independently invented integration.} 13 | spec.homepage = "http://leibniz.cc" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "test-kitchen", "~> 1.0" 22 | spec.add_dependency "kitchen-vagrant" 23 | spec.add_dependency "thor" 24 | spec.add_dependency "cucumber" 25 | spec.add_dependency "chef", "> 11" 26 | 27 | spec.add_development_dependency "bundler", "~> 1.3" 28 | spec.add_development_dependency 'aruba', '~> 0.5' 29 | spec.add_development_dependency "rake" 30 | spec.add_development_dependency "pry" 31 | 32 | end 33 | -------------------------------------------------------------------------------- /bin/leibniz: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.join(File.dirname(__FILE__), %w{.. lib}) 4 | require 'thor' 5 | require 'leibniz' 6 | require 'pathname' 7 | 8 | # Trap interrupts to quit cleanly. See 9 | # https://twitter.com/mitchellh/status/283014103189053442 10 | Signal.trap("INT") { exit 1 } 11 | 12 | class LeibnizCli < Thor 13 | include Thor::Actions 14 | 15 | desc 'init', 'Set up a project ready for Leibniz Acceptance Testing' 16 | option :driver, :default => 'vagrant' 17 | def init 18 | set_template_root 19 | create_leibniz_yaml(options[:driver]) 20 | create_skeleton_feature 21 | append_to_gitignore(".leibniz/") 22 | append_to_gitignore(".kitchen/") 23 | end 24 | 25 | desc "version", "Print Leibniz's version information" 26 | def version 27 | say "Leibniz version #{Leibniz::VERSION}" 28 | end 29 | map %w(-v --version) => :version 30 | 31 | private 32 | 33 | def set_template_root 34 | LeibnizCli.source_root Pathname.new(File.expand_path('../../templates', __FILE__)) 35 | end 36 | 37 | def create_leibniz_yaml(driver) 38 | template '.leibniz.yml.erb', '.leibniz.yml', {:driver => driver} 39 | end 40 | 41 | def create_skeleton_feature 42 | directory 'features' 43 | end 44 | 45 | def append_to_gitignore(line) 46 | create_file(".gitignore") unless File.exists?(File.join(destination_root, ".gitignore")) 47 | if IO.readlines(File.join(destination_root, ".gitignore")).grep(%r{^#{line}}).empty? 48 | append_to_file(".gitignore", "#{line}\n") 49 | end 50 | end 51 | 52 | end 53 | 54 | LeibnizCli.start 55 | -------------------------------------------------------------------------------- /features/leibniz_init.feature: -------------------------------------------------------------------------------- 1 | Feature: Add Leibniz testing to an existing project 2 | In order to get started with Leibniz 3 | As an infrastructure developer 4 | I want to run a command to initialize my project 5 | 6 | Scenario: Displaying help 7 | When I run `leibniz help init` 8 | Then the output should contain: 9 | """ 10 | Usage: 11 | leibniz init 12 | """ 13 | And the exit status should be 0 14 | 15 | Scenario: Running leibniz init within a project 16 | When I run `leibniz init` 17 | Then the exit status should be 0 18 | And the file ".leibniz.yml" should contain: 19 | """ 20 | --- 21 | driver: vagrant 22 | network: 10.2.3.0/24 23 | suites: 24 | - name: leibniz 25 | run_list: [] 26 | data_bags_path: "test/integration/default/data_bags" 27 | """ 28 | And a directory named "features/support" should exist 29 | And the file "features/support/env.rb" should contain "require 'leibniz'" 30 | And the file "features/learning_leibniz.feature" should contain: 31 | """ 32 | Given I have provisioned the following infrastructure: 33 | """ 34 | And a directory named "features/step_definitions" should exist 35 | And the file "features/step_definitions/learning_steps.rb" should contain "@infrastructure.converge" 36 | And the file ".gitignore" should contain ".leibniz/" 37 | 38 | Scenario: Specifying the dummy driver when running leibniz init 39 | When I run `leibniz init --driver dummy` 40 | Then the exit status should be 0 41 | And the file ".leibniz.yml" should contain "driver: dummy" 42 | 43 | Scenario: Using the dummy driver 44 | Given I have a wrapper cookbook called "leibniz" 45 | And I elect to use the "dummy" driver 46 | When I successfully run `cucumber` 47 | Then the file ".kitchen/logs/leibniz-learning.log" should match /Dummy..Create on instance.+id=>"leibniz-learning-\d+"}/ 48 | -------------------------------------------------------------------------------- /lib/leibniz.rb: -------------------------------------------------------------------------------- 1 | require 'pry' 2 | require 'leibniz/version' 3 | require 'kitchen' 4 | require 'forwardable' 5 | require 'ipaddr' 6 | 7 | module Kitchen 8 | class Config 9 | def new_logger(suite, platform, index) 10 | name = instance_name(suite, platform) 11 | Logger.new( 12 | :color => Color::COLORS[index % Color::COLORS.size].to_sym, 13 | :logdev => File.join(log_root, "#{name}.log"), 14 | :level => Util.to_logger_level(self.log_level), 15 | :progname => name 16 | ) 17 | end 18 | end 19 | end 20 | 21 | module Leibniz 22 | 23 | def self.build(specification) 24 | leibniz_yaml = YAML.load_file(".leibniz.yml") 25 | loader = KitchenLoader.new(specification, leibniz_yaml) 26 | config = Kitchen::Config.new(:loader => loader) 27 | Infrastructure.new(config.instances) 28 | end 29 | 30 | class Infrastructure 31 | 32 | def initialize(instances) 33 | @nodes = Hash.new 34 | instances.each do |instance| 35 | @nodes[instance.name.sub(/^leibniz-/, '')] = Node.new(instance) 36 | end 37 | end 38 | 39 | def [](name) 40 | @nodes[name] 41 | end 42 | 43 | def converge 44 | @nodes.each_pair { |name, node| node.converge } 45 | end 46 | 47 | def destroy 48 | @nodes.each_pair { |name, node| node.destroy } 49 | end 50 | 51 | end 52 | 53 | class Node 54 | 55 | extend Forwardable 56 | 57 | def_delegators :@instance, :create, :converge, :setup, :verify, :destroy, :test 58 | 59 | def initialize(instance) 60 | @instance = instance 61 | end 62 | 63 | def ip 64 | instance.driver[:ipaddress] 65 | end 66 | 67 | private 68 | 69 | attr_reader :instance 70 | end 71 | 72 | class KitchenLoader 73 | 74 | def initialize(specification, config) 75 | @config = config 76 | @last_octet = @config['last_octet'] 77 | @platforms = specification.hashes.map do |spec| 78 | create_platform(spec) 79 | end 80 | @suites = specification.hashes.map do |spec| 81 | create_suite(spec) 82 | end 83 | end 84 | 85 | def read 86 | { 87 | :driver_plugin => @config['driver'], 88 | :platforms => platforms, 89 | :suites => suites 90 | } 91 | end 92 | 93 | private 94 | 95 | attr_reader :platforms, :suites 96 | 97 | def create_suite(spec) 98 | suite = Hash.new 99 | suite[:name] = @config['suites'].first['name'] 100 | suite[:run_list] = @config['suites'].first['run_list'] 101 | suite[:data_bags_path] = @config['suites'].first['data_bags_path'] 102 | suite 103 | end 104 | 105 | 106 | def create_platform(spec) 107 | distro = "#{spec['Operating System']}-#{spec['Version']}" 108 | ipaddress = IPAddr.new(@config['network']).succ.succ.to_s 109 | platform = Hash.new 110 | platform[:name] = spec["Server Name"] 111 | platform[:driver_config] = Hash.new 112 | platform[:driver_config][:box] = "opscode-#{distro}" 113 | platform[:driver_config][:box_url] = "https://opscode-vm-bento.s3.amazonaws.com/vagrant/opscode_#{distro}_provisionerless.box" 114 | platform[:driver_config][:network] = [["private_network", {:ip => ipaddress}]] 115 | platform[:driver_config][:require_chef_omnibus] = spec["Chef Version"] || true 116 | platform[:driver_config][:ipaddress] = ipaddress 117 | platform[:run_list] = spec["Run List"].split(",") 118 | platform 119 | end 120 | end 121 | end 122 | 123 | -------------------------------------------------------------------------------- /WISHLIST.md: -------------------------------------------------------------------------------- 1 | # Things I'd like this tool to have 2 | ## in no particular order 3 | 4 | ### Jenkins 5 | 6 | Or some other other CI workflow. At present we have a pretty slick Continuous Deployment pipeline for our code. It would be great if we could run our infrastructure code through the same sort of system, because at the moment, although we have tools like Etsy's [knife-spork](https://github.com/jonlives/knife-spork) to guard against idiocy, it's basically gated by me. Fallible, error-prone me. 7 | 8 | I know Zach mentioned something about a [Jenkins workflow](https://github.com/Atalanta/cucumber-chef/issues/101) a while ago, but I never heard anything more. 9 | 10 | #### Current Status 11 | 12 | - A project could be checked out and cucumber run by Jenkins or Travis 13 | - However at present Leibniz only knows how to build machines via TK with the Vagrant driver 14 | - This would mean we couldn't run it on Travis or on a 'cloud' Jenkins box; ie the Jenkins box would need to run Vagrant 15 | - that said I'm not sure whether Vagrant itself would be able to use the EC2 'provider'? 16 | 17 | #### Next Steps 18 | 19 | - On a physical box, prove that we can run a Leibniz job from Jenkins 20 | - Try to get Leibniz to with TK to with the EC2 driver (or LXC / Docker) 21 | 22 | ### Test configuration per-project 23 | 24 | [I mooted this back in April](https://github.com/Atalanta/cucumber-chef/pull/117) but then I went on holiday and kind of forgot about it. The somewhat lashed-together solution I came up with there looks a bit clunky now, but I think the idea still holds. Right now, I'm having to keep several different Labfiles around and symlink the correct one each time. 25 | 26 | #### Current Status 27 | 28 | - Leibniz simply provides an interface to Test Kitchen, and passes over a run list. 29 | - This means we can have features / steps per project or per cookbook - whatever works 30 | 31 | #### Next Steps 32 | 33 | - Demonstrate that this can work with Librarian? 34 | 35 | 36 | ### Looser coupling of moving parts 37 | 38 | Here's the thing: the most complex project I'm currently managing with cuke-chef has 5 different types of node. In order to test things like Chef-search correctly, I need to spin up one of each from the Labfile, which incurs a huge first-run penalty. Subsequent runs are better, but still incredibly time-consuming as it seems to provision all the nodes on each run (which is not unreasonable, I guess). 39 | 40 | Maybe there are already clever things I could with mocking and so on but I've never really looked into that. But whatever, being able to exercise only the required node(s) on a given test run (without commenting-out whole blocks from the Labfile, which is my current anti-pattern) would be splendid. 41 | 42 | #### Current Status 43 | 44 | - Acceptance tests shouldn't mock - we want to exercise all the pieces 45 | - TK allows us to use Chef Zero - which gives us an in memory Chef server, including search, which is really really quick 46 | - At lower levels (chefspec) we can stub the search and return data 47 | 48 | #### Next Steps 49 | 50 | - Demonstrate (and document) how to make Leibniz use the chef-zero provisioner 51 | 52 | ### Full-stack testing 53 | 54 | Right now, the way I'm using cuke-chef isn't at all BDD. My steps say things like 55 | 56 | ``` 57 | Scenario: www vhost is correct 58 | * file "/var/www/www/current/vhost" should contain 59 | """ 60 | upstream www { 61 | server 127.0.0.1:3020; 62 | } 63 | 64 | ``` 65 | 66 | whereas what I *should* care about would be more like 67 | 68 | ``` 69 | Scenario: Home page 70 | Given I am on "the home page" 71 | Then I should see "All work and no play makes Jack a dull boy" 72 | ``` 73 | 74 | Now of course this sort of stuff is tested in the apps themselves so maybe I'm looking at this from the wrong end, or maybe others are already doing this with cuke-chef and and I'm just Doing It Wrong. 75 | 76 | But this, combined with the *Test configuration per-project* idea from above, would maybe let us test the entire stack from base OS to working app, which has a certain appeal. 77 | 78 | #### Current Status 79 | 80 | - The lower-level 'vhost is correct' steps should be run post-converge on the node under test using TK + whatever you want to write tests with (I recommend serverspec) 81 | - The high-level BDD-style acceptance tests (the homepage does what it should) is precisely what Leibniz & Cucumber are for 82 | 83 | # Annoyances with current cucumber-chef 84 | 85 | * Occasional failure of packet-passing 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Climate](https://codeclimate.com/github/Atalanta/leibniz.png)](https://codeclimate.com/github/Atalanta/leibniz) 2 | # Leibniz 3 | 4 | Leibniz is simple utility which provides the ability to launch 5 | infrastructure using Test Kitchen, and run acceptance tests against 6 | that infrastructure. It is designed to be used as part of a set of 7 | Cucumber / Gherkin features. 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | gem 'leibniz' 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install leibniz 22 | 23 | ## Usage 24 | 25 | ### The Leibniz CLI 26 | 27 | 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: 28 | 29 | leibniz init 30 | 31 | ### Getting Started 32 | 33 | Leibniz takes the view that acceptance testing of infrastructure 34 | should be performed from the outside in, and make assertions from the 35 | perspective of an external user consuming the delivered 36 | infrastructure. 37 | 38 | To get started, you will need to write some features and some steps. 39 | Depending on how you build your infrastructure (at present the assumed 40 | approach is Berkshelf and wrapper cookbooks, but there's no reason why 41 | it wouldn't work with Librarian-chef, or some other approach). 42 | 43 | #### Using Berkshelf 44 | 45 | Assuming you have Berkshelf installed, you can use the in-built 46 | cookbook generator to create your wrapper cookbook. The alternative 47 | is to create a cookbook directory, or use knife to create a cookbook, 48 | and then add 'berkshelf' to a Gemfile, and run bundle install, followed by berks init. 49 | 50 | Either way, once you have a cookbook which has been 'berksified' you 51 | will have something that looks like this: 52 | 53 | ``` 54 | . 55 | ├── Berksfile 56 | ├── Gemfile 57 | ├── LICENSE 58 | ├── README.md 59 | ├── Thorfile 60 | ├── Vagrantfile 61 | ├── attributes 62 | ├── chefignore 63 | ├── definitions 64 | ├── files 65 | │   └── default 66 | ├── libraries 67 | ├── metadata.rb 68 | ├── providers 69 | ├── recipes 70 | │   └── default.rb 71 | ├── resources 72 | └── templates 73 | └── default 74 | ``` 75 | 76 | #### Writing your first feature 77 | 78 | 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. 79 | 80 | First we need to create a directory to contain our features, then write our first feature: 81 | 82 | ``` 83 | mkdir features 84 | cd features 85 | vi generic_webpage.feature 86 | ``` 87 | 88 | The feature you write needs to have a `Background` section like this: 89 | 90 | ``` 91 | Background: 92 | 93 | Given I have provisioned the following infrastructure: 94 | | Server Name | Operating System | Version | Chef Version | Run List | 95 | | generic_webpage | ubuntu | 12.04 | 11.8.0 | generic_webpage::default | 96 | And I have run Chef 97 | ``` 98 | 99 | 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: 100 | 101 | - `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. 102 | - `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. 103 | - `Version` - this is version of the Operating System. See the Bento website for an up-to-date specification of the available versions. 104 | - `Chef Version` - this is the version of the Chef 'client' software to be installed. 105 | - `Run List` - this is the Chef run list which will be used when the node is converged. 106 | 107 | 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: 108 | 109 | ``` 110 | require 'leibniz' 111 | require 'rspec/expectations' 112 | World(RSpec::Matchers) 113 | ``` 114 | 115 | Now create your step definitions: 116 | 117 | ``` 118 | mkdir features/step_definitions 119 | vi features/step_definitions/generic_webpage_steps.rb 120 | ``` 121 | 122 | The following steps will build and converge the infrastructure described in the table: 123 | 124 | ``` 125 | Given(/^I have provisioned the following infrastructure:$/) do |specification| 126 | @infrastructure = Leibniz.build(specification) 127 | end 128 | 129 | Given(/^I have run Chef$/) do 130 | @infrastructure.destroy 131 | @infrastructure.converge 132 | end 133 | ``` 134 | 135 | At present, Leibniz only knows how to provision infrastructure using 136 | the Vagrant driver. By default this will assume you have Virtualbox 137 | on the system where you are running Cucumber. A top priority is to 138 | support other Kitchen drivers, which will enable infrastructure to be 139 | provisioned on cloud platforms, via LXC or Docker, or just with 140 | Vagrant. 141 | 142 | Once you have your feature, env.rb and steps in place, you can run 143 | `cucumber`. This will build the infrastructure you described using 144 | Chef. 145 | 146 | You may find it useful to tail the logs during this process: 147 | 148 | ``` 149 | tail -f .kitchen/logs/leibniz-generic-webpage.log 150 | ``` 151 | 152 | If all goes well, you should see something like: 153 | 154 | ``` 155 | Feature: Serve a generic webpage 156 | 157 | In order to demonstrate how Leibniz works 158 | As an infrastructure developer 159 | I want to be able to serve a generic webpage and test it 160 | 161 | Background: # features/crap_webpage.feature:7 162 | Given I have provisioned the following infrastructure: # features/step_definitions/generic_webpage_steps.rb:1 163 | | Server Name | Operating System | Version | Chef Version | Run List | 164 | | generic_webpage | ubuntu | 12.04 | 11.8.0 | generic_webpage::default | 165 | Using generic_webpage (0.1.0) from metadata 166 | And I have run Chef # features/step_definitions/generic_webpage_steps.rb:5 167 | 168 | 0 scenarios 169 | 2 steps (2 passed) 170 | 0m58.613s 171 | ``` 172 | 173 | 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: 174 | 175 | ``` 176 | Scenario: Infrastructure developer can see generic webpage 177 | Given a URL "http://generic-webpage.com" 178 | When I browse to the URL 179 | Then I should see "This is a generic webpage" 180 | ``` 181 | 182 | When we run cucumber again, we should be told that our steps are undefined. Cucumber will suggest some snippets we can use: 183 | 184 | ``` 185 | You can implement step definitions for undefined steps with these snippets: 186 | 187 | Given(/^a URL "(.*?)"$/) do |arg1| 188 | pending # express the regexp above with the code you wish you had 189 | end 190 | 191 | When(/^I browse to the URL$/) do 192 | pending # express the regexp above with the code you wish you had 193 | end 194 | 195 | Then(/^I should see "(.*?)"$/) do |arg1| 196 | pending # express the regexp above with the code you wish you had 197 | end 198 | ``` 199 | 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. 200 | 201 | 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. 202 | 203 | We now need to write the implementation of our acceptance tests: 204 | 205 | - Given a URL "http://generic-webpage.com" 206 | - When I browse to the URL 207 | - Then I should see "This is a generic webpage" 208 | 209 | The first step simply requires us to capture a host header that we can use as part of an http client: 210 | 211 | ``` 212 | Given(/^a URL "(.*?)"$/) do |url| 213 | @host_header = url.split("/").last 214 | end 215 | ``` 216 | 217 | The second step requires us to use an http client. There are many options available for this, Faraday is a simple one: 218 | 219 | ``` 220 | When(/^I browse to the URL$/) do 221 | connection = Faraday.new(url: "http://#{@infrastructure['generic-webpage'].ip}", headers: { 'Host' => @host_header }) do |faraday| 222 | faraday.adapter Faraday.default_adapter 223 | end 224 | @page = connection.get('/').body 225 | end 226 | ``` 227 | 228 | Note: the ip of the infrastructure we have built is available as part of the @infrastructure object returned by Leibniz. 229 | 230 | The final step is a simple rspec expectation: 231 | 232 | ``` 233 | Then(/^I should see "(.*?)"$/) do |content| 234 | expect(@page).to match /#{content}/ 235 | end 236 | ``` 237 | 238 | 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. 239 | 240 | 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: 241 | 242 | ``` 243 | Feature: Serve a generic webpage 244 | 245 | In order to demonstrate how Leibniz works 246 | As an infrastructure developer 247 | I want to be able to serve a generic webpage and test it 248 | 249 | Background: # features/generic_webpage.feature:7 250 | Given I have provisioned the following infrastructure: # features/step_definitions/generic_webpage_steps.rb:3 251 | | Server Name | Operating System | Version | Chef Version | Run List | 252 | | generic_webpage | ubuntu | 12.04 | 11.8.0 | generic_webpage::default | 253 | Using generic_webpage (0.1.0) 254 | Using apt (2.3.0) 255 | Using apache2 (1.8.4) 256 | And I have run Chef # features/step_definitions/generic_webpage_steps.rb:7 257 | 258 | Scenario: Infrastructure developer can see generic webpage # features/generic_webpage.feature:14 259 | Given a URL "http://generic-webpage.com" # features/step_definitions/generic_webpage_steps.rb:12 260 | When I browse to the URL # features/step_definitions/generic_webpage_steps.rb:16 261 | Then I should see "This is a generic webpage" # features/step_definitions/generic_webpage_steps.rb:23 262 | 263 | 1 scenario (1 passed) 264 | 5 steps (5 passed) 265 | 1m25.227s 266 | ``` 267 | 268 | ## Contributing 269 | 270 | 1. Fork it 271 | 2. Create your feature branch (`git checkout -b my-new-feature`) 272 | 3. Commit your changes (`git commit -am 'Add some feature'`) 273 | 4. Push to the branch (`git push origin my-new-feature`) 274 | 5. Create new Pull Request 275 | --------------------------------------------------------------------------------