Opening remarks

All my previous posts were about choreography, deployment, topology, and more recently about an attempt to include AI in those systems. This post is a bit apart, because I’m facing a new challenge in my job which is to implement BDD in a CI chain. Therefore, I’m using this blog as a reminder of what I did personally. The following of the Markov saga will come again later.

Introduction

Wikipedia defines the word contract like this:

A contract is a voluntary arrangement between two or more parties that is enforceable at law as a binding legal agreement.

If law usually describes what you can and cannot do, a contract is more likely to describe what’s you are expected to do.

A law’s goal is not only to give rules to follow, but also to maintain a stability in an ecosystem. In IT there are laws, that may be implicit, didactic, empiric, … but the IT with all its laws should not dictate the expected behavior of the customer. But how often have you heard:

“those computer stuffs are not for me, just get the thing done”

“we’ve always done it this way”

There are laws that cannot be changed, but the contract between a customer and its provider could and should evolve.

In IT, like everywhere else where a customer/provider relationship exists, a special need is formalized via specifications. Specifications are hard to follow, but even more they’re hard to evaluate.

The __B__ehavior __D__riven __D__evelopment is the assurance that everything have been made respectfully i with the contract ²that has been established between the parties (customers and providers). To do things right, this contract should be established at the very beginning.

Hence, every single item must be developed with all the features of the contract in mind. And then, it should be possible to use automation to perform the tests of behaviour, so that the developer can see if the contract is fulfilled, and if, for example, no regression has been introduced.

In a continuous integration chain, this is an essential piece that can be use to fully automate the process of delivery.

Gherkin

To express the specification in a way that can be both human and computer readable, the easiest way is to use a special dedicated language.

Such a language is known as DSL ( Domain Specific Language).

Gherkin is a DSL that lets you describe software’s behaviour without dealing how that behaviour is implemented

The behaviour is a scenario detailed as a set of features. A feature is a human readable English (or another human language among 37 implemented languages) text file with a bunch of key words in it (eg: Given, And, When, Then,…). Those words do not only help the writer of the feature to organize its idea, but they are used by the Gherkin processor to localize the test of the feature in the code. Of course, there is no magic in it: the test must have been implemented manually.

And here comes Cucumber

The historic Gherkin processor is called Cucumber. It’s a Ruby implementation of the Gherkin DSL. Its purpose is to read a scenario, and to localize the Ruby code that is implementing the all the tests corresponding to the scenario. Finally it executes the code, and for each feature it simply says ok or ko.

Easy.

Nowadays there are many implementation of Gherkin parser for different languages, but in this post I will stick to the Cucumber.

Let’s play

Let’s see how we can implement a basic behaviour driver development with the help of cucumber and Ruby. The idea here is not to test a Ruby development, but instead to use ruby to validate a shell script. That’s the main reason why I stick to Ruby (instead of GO which I know better). The Go implementation (GoDoc, GoConvey, …) relies on go test and therefore are related to a pure GO development. Of course I could do a complete GO development to encapsulate my scripts, but that’s not the point; for my purpose, a scripting language is a better choice.

Ruby is a scripting language and all the tests implemented here are neither dependent on the Ruby test framework nor on RSpec.

I will write a script that will deploy an EC2 instance via vagrant-aws and install an Openvpn instance on it.

The scenario

The customer point of view

With my role of customer, the feature I’m expecting is:

  • Given the execution of the program, and waiting for it to be successful
  • Then I may be able to watch netflix US from France.

The feature may be:

 1Feature: I want a program that
 2  will simply allows me to watch netflix US
 3
 4  Scenario: I want to watch netflix
 5     Given I am on my chromebook
 6     And I have access to the shell
 7     When I want to watch netflix
 8     And I launch a program from the command line
 9     And it displays ready
10     Then I open a navigator windows on http://www.netflix.com
11     And I can watch Grey's anatomy (which is not available in france)

The architect point of view

As an architect the implementation I’m thinking of is

  • start an EC2 instance (I will not create it in this post)
  • register it to my DNS (with blog-test.owulveryck.info)
  • install Openvpn
  • configure Openvpn to make it accessible via blog-test.owulveryck.info

The developer point of view

And as a developer, I’m thinking about using vagrant-aws to perform the tasks. All the implementation will be based on a Vagrant file and a provisioning script. The vagrant file will be evaluated by vagrant up on CLI (aka in the real world, by the end user) and the same vagrant file will be evaluated within my cucumber scripts.

Therefore I can say that I am doing BDD/TDD for a configuration management and provisioning.

The basic feature

I will describe here a single feature, just for testing purpose.

Setting up the Ruby environment

I will use the Ruby implementation of cucumber. To install it, assuming that we have a gem installed, just run this command

1# gem install cucumber

This will load all the required dependencies. It may also be a good idea to use bundle if we plan to do further development of the steps in ruby.

The test environment with bundler

The whole development will run with the help of bundler (and RVM). See this post for more explanation on how I set it up on my Chromebook.

1> mkdir /media/removable/Lexar/tools/vpn-blog
2> cd /media/removable/Lexar/tools/vpn-blog
3> rvmrc --create 2.2.0@vpn-blog
4> source .rvmrc
5> gem install bundler -v 1.5.2 
6> bundle init
7Writing new Gemfile to /home/chronos/user/gherkin/Gemfile

the Gemfile

Let’s add the cucumber, vagrant (as installed in a previous post ), and vagrant-aws dependencies in the Gemfile:

1> cat Gemfile
2source "https://rubygems.org"
3
4gem "vagrant", :path => "/media/removable/Lexar/tools/vagrant"
5gem "vagrant-aws"
6gem "bundler", "1.5.2"
7gem "cucumber"

and then install the bundle:

 1> bundle _1.5.2_ install
 2Resolving dependencies...
 3Using builder 3.2.2
 4Using gherkin 3.2.0
 5Using cucumber-wire 0.0.1
 6Using diff-lcs 1.2.4
 7Using multi_json 1.7.9
 8Using multi_test 0.1.2
 9Using bundler 1.11.2
10Using cucumber-core 1.4.0
11Using cucumber 2.3.3
12...
13Bundle complete! 1 Gemfile dependency, 9 gems now installed.
14Use `bundle show [gemname]` to see where a bundled gem is installed.

And now let’s run cucumber within the bundle:

1> bundle _1.5.2_ exec cucumber
2No such file or directory - features. You can use `cucumber --init` to get started.

The skeleton of the test

First, as requested by cucumber, let’s initialize a couple of files in the directory to be “cucumber compliant”. Cucumber do have a helpful init function. Let’s run it now:

1bundle _1.5.2_ exec cucumber --init
2  create   features
3  create   features/step_definitions
4  create   features/support
5  create   features/support/env.rb

Adding the feature file

In the features/ directory, I create a file basic_feature.feature which contains the YAML we wrote earlier, then I run cucumber again.

 1$ bundle _1.5.2_ exec cucumber
 2Feature: I want a program that
 3  will simply allows me to watch netflix US
 4  
 5  Scenario: I want to watch netflix                                   # features/basic_feature.feature:4
 6    Given I am on my chromebook                                       # features/basic_feature.feature:5
 7    And I have access to the shell                                    # features/basic_feature.feature:6
 8    When I want to watch netflix                                      # features/basic_feature.feature:7
 9    And I launch a program on the command line                        # features/basic_feature.feature:8
10    And it displays ready                                             # features/basic_feature.feature:9
11    Then I open a navigator windows on http://www.netflix.com         # features/basic_feature.feature:10
12    And I can watch Grey's anatomy (which is not available in france) # features/basic_feature.feature:11
13                                
141 scenario (1 undefined)
157 steps (7 undefined)
160m0.054s
17
18You can implement step definitions for undefined steps with these snippets:
19
20Given(/^I am on my chromebook$/) do
21  pending # Write code here that turns the phrase above into concrete actions
22end
23...

We notice that the feature has been read and understood correctly by cucumber. ON top of that Cucumber gives the skeleton of a ruby implementation for the tests.

I will copy all the ruby code in its own file:

1# cat > features/step_definitions/tests.rb
2Given(/^I am on my chromebook$/) do
3  pending # Write code here that turns the phrase above into concrete actions
4  end
5...

And run cucumber once more:

 1Feature: I want a program that
 2  will simply allows me to watch netflix US
 3
 4  Scenario: I want to watch netflix                                   # features/basic_feature.feature:4
 5      Given I am on my chromebook                                       # features/step_definitions/tests.rb:1
 6        TODO (Cucumber::Pending)
 7        ./features/step_definitions/tests.rb:2:in `/^I am on my chromebook$/'
 8        features/basic_feature.feature:5:in `Given I am on my chromebook'
 9      And I have access to the shell                                    # features/step_definitions/tests.rb:5
10      When I want to watch netflix                                      # features/step_definitions/tests.rb:9
11      And I launch gonetflix.sh                                         # features/step_definitions/tests.rb:13
12      And it displays ready                                             # features/step_definitions/tests.rb:17
13      Then I open a navigator windows on http://www.netflix.com         # features/step_definitions/tests.rb:21
14      And I can watch Grey's anatomy (which is not available in france) # features/step_definitions/tests.rb:25
15      
161 scenario (1 pending)
177 steps (6 skipped, 1 pending)
180m0.041s`

Cool, the framework is ok. Now let’s actually implement the scenario and the tests

Implementation of the “Given” keywords

There is not much to say about the Given keyword. I can test that I am really on my Chromebook but that does not make any sense. I will skip this test by not implementing anything in the function.

Implementation of the “When” keyword

The actual execution of the “When” is the execution of the Vagrant file. It will start the EC2 instance and provision the VPN I also need to mount the VPN locally afterwards

1#!/usr/bin/env ruby
2require "vagrant"
3
4# Starting the EC2 instance (running the vagrantfile)
5env = Vagrant::Environment.new
6env.cli("up")
7# Starting OpenVPN locally
8`sudo openvpn --mktun --dev tun0 && sudo openvpn --config ~/Downloads/client.ovpn --dev tun0`

(trying to) Implement the netflix test with selenium

To test the access, instead of faking my browser with curl, I will use the selenium tool. So I add it to my Gemfile and bundle update it (informations comes from this starterkit):

1$ echo 'gem "selenium-cucumber"' >> Gemfile
2$ echo 'gem "selenium-webdriver"' >> Gemfile
3$ echo 'gem "require_all"' >> Gemfile
4$ bundle _1.5.2_ update 

Then I need to create a special file in the support subdirectory to define a bunch of objects:

 1# cat features/support/env.rb
 2require 'selenium-webdriver'
 3require 'cucumber'
 4
 5require 'require_all'
 6
 7require_all 'lib'
 8
 9Before do |scenario|
10    @browser = Browser.new(ENV['DRIVER'])
11    @browser.delete_cookies
12end
13
14After do |scenario|
15    @browser.driver.quit
16end

I’m also adding the files from the starterkit in the lib subdirectory.

As I am developing on my Chromebook, I also need the chromedriver

Too bad chromedriver relies on the libX11 that cannot be installed on my Chromebook / end of show for selenium on the Chromebook… for now

Note I will continue with the development, but be aware that I won’t be able to test it until I am on a true linux box with the chromedriver installed

1Then(/^I open a navigator windows on (.*?)$/) do |arg1|
2  @browser.open_page("http://www.netflix.com")
3end
4
5Then(/^I can watch Grey's anatomy \(which is not available in france\)$/) do
6  @browser.open_page("http://www.netflix.com/idtogreysanatomy")
7end

The actual implementation of the scenario

What I need to do is to implement the scenario. Not the test scenario, the real one; the one that will actually allows me to launch my ec2 instance, configure and start Openvpn.

As I said before, I will use vagrant-aws to do so.

Note vagrant was depending on bsdtar, and I’ve had to install it manually from source:

(tar xzvf libarchive-3.1.2.tar.gz && ... && ./configure --prefix=/usr/local && make install clean)

Installing vagrant-aws plugin

The vagrant-aws plugin has been installed by the bundler because I’ve indicated it as a dependency in the Gemfile. But, I will have to have it as a requirement in the Vagrantfile because I’m not using the “official vagrant” and that I am running in a bundler environment:

Vagrant’s built-in bundler management mechanism is disabled because Vagrant is running in an external bundler environment. In these cases, plugin management does not work with Vagrant. To install plugins, use your own Gemfile. To load plugins, either put the plugins in the plugins group in your Gemfile or manually require them in a Vagrantfile.

Installing the base box

The documentation says that the quickest way to get started is to install the dummy box. That’s what I did:

1$ bundle _1.5.2_ exec vagrant box add dummy https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box
2...
3==> box: Successfully added box 'dummy' (v0) for 'aws'!

The Vagrantfile

The initial Vagrantfile looks like this:

 1require "vagrant-aws"
 2Vagrant.configure("2") do |config|
 3  config.vm.box = "dummy"
 4
 5  config.vm.provider :aws do |aws, override|
 6    aws.access_key_id = "YOUR KEY"
 7    aws.secret_access_key = "YOUR SECRET KEY"
 8    aws.session_token = "SESSION TOKEN"
 9    aws.keypair_name = "KEYPAIR NAME"
10
11    aws.ami = "ami-7747d01e"
12
13    override.ssh.username = "ubuntu"
14    override.ssh.private_key_path = "PATH TO YOUR PRIVATE KEY"
15  end
16end

So all the rest in the basic implementation of the vagrant file and the provisioning.sh for the Openvpn configuration. but that goes far behind the topic of this post which was to introduce myself to BDD and TDD.

Conclusion

I’ve learned a lot about the ruby and cucumber environment in this post. Too bad I couldn’t end with a fully running example because of my Chromebook.

Anyway the expected results were for me to:

  • learn about BDD
  • learn about cucumber
  • learn about Ruby
  • learn about vagrant

I can say that I’ve reach my goals anyway. I will try to finish the implementation on a true Linux box locally, or on my Macbook if I have time to do so.