RSpec For Ops

I’ve noticed a lot of interest in RSpec from ops folks lately, likely driven by infracode testing tools like Kitchen, Beaker, and Serverspec. I’ve also noticed a lot of ops folks using rspec without really understanding some of the fundamentals. This results in some suboptimal rspec code, and generally lots of headaches for newcomers to rspec. This blog post is the first in a series attempting to outline some of the basics of test driven development with rspec from the perspective of an Op :) I’ll start by outlining rspec fundamentals, another post will follow with some (hopefully) relatable examples.

RSpec Essentials

RSpec is a behavior driven development (BDD) framework for Ruby. BDD focuses on the behavior and expected outcomes of your code. RSpec provides a simple domain-specific language that employs natural-lanaguage constructs for describing tests. As a result, BDD code is often very easy to read with inputs and expectations being very easy to identify. RSpec offers several primitives to model the behavior of your code including groups, examples, contexts, expectations, and matchers.

Examples and Groups

One of the most fundamental primitives in rspec is the example. An example is the description of a behavior you expect your code to exhibit. Examples are declared using the it method and are often grouped together logically inside an (very creatively named) example group. Examples are comprised of a subject and an expectation. Example groups are declared with the describe method. Example groups make up your application’s specification; they often end up looking a lot like lists of acceptance criteria. Here’s a simple example:

it { is_expected.to be true }

This code can be read as “it is expected to be true” and literally means that the subject of the test (we’ll go into more detail in a minute) should return true. I didn’t set a subject in the example above, but we can do that with the let method:

let(:subject) { true }

it { is_expected.to be true }

The let method creates a memoized helper method set to the value passed in the block. A memoized helper method is basically an attr_reader on an instance variable. Here’s what the above let method effectively does under the hood:

def subject
  @subject ||= true
end

The let method creates a new method based on the name you passed it as an argument. The code passed to the let method’s block is then lazily evaluated and cached in the instance variable @subject. Lazy evaluation means that the code associated with this method is not evaluated until it is called the first time. This is in contrast to eager evaluation where the code is evaluated as soon as it is bound to a variable. This allows you to set some state that persists inside an example group, this also means that you can only use the let method inside an example group. Let’s fix our example code:

describe 'Truthiness' do
  let(:subject) { true }

  it { is_expected.to be true }
end

Here we have an example group named Truthiness, we’ve set the subject to true, and we’ve created an example with the expectation that our subject is true. Let’s run this code and see what the output is:

Truthiness
  should equal true

Finished in 0.00107 seconds (files took 0.08412 seconds to load)
1 example, 0 failures

Expectations and Matchers

An example is just a list of behaviors expected of our subject. Our example has one expectation: that the subject should be true. Our example above could also be expressed using the expect method:

describe 'Truthiness' do
  let(:subject) { true }

  it { expect(subject).to be true }
end

This doesn’t read as smoothly as it did with the is_expected method, which is just expect with an implicit subject. We can make this example more readable by passing a descriptive title to the it method:

describe 'Truthiness' do
  let(:subject) { true }

  it 'should behave truthy' do
    expect(subject).to be true
  end
end

Now this example is easier to read. We can see that our subject should behave truthy and we have a list of expectations that need to be met for our example to pass. Let’s add an expectation that our subject is not false:

describe 'Truthiness' do
  let(:subject) { true }

  it 'should behave truthy' do
    expect(subject).to be true
    expect(subject).not_to be false
  end
end

These code examples are available on my GitHub here. The example above can be found in spec/truthiness_spec.rb

As you can see, examples can contain any number of expectations. You should use as many expectations as necessary to validate the behavior you’re testing in an example. One element of an expectation that we haven’t yet discussed is the matcher, which is really the heart of an expectation. Our example above only uses the be matcher but there are many others. Matchers describe the behavior you expect your subject to exhibit. In our example, we are using the be matcher to compare our subject’s identity with our matcher.

There are lots of built-in matchers; common matchers include be, eq, exist, include, match, start_with, and respond_to. RSpec has a robust matcher API that makes it very easy to write custom matchers. Infracode frameworks built on RSpec like Serverspec include a number of domain-specific matchers. Let’s take a look at some different kinds of matchers:

describe 'Hello world!' do
  it 'should say hello' do
    expect(subject).to match /Hello world/
    expect(subject).not_to match /Goodbye world/
    expect(subject).to eq 'Hello world!'
  end
end

In this example group we are using the match and eq matchers. Notice I didn’t explicitly set a subject here, we’re relying on the implicit subject set by the argument we passed to the describe method. The match matcher compares your subject with a regular expression, in this case /Hello world/. You can set a negative expectation by using the not_to method on our expectation. In this example, we want to make sure that we’re saying “hello” so we want to make sure our subject does not match /Goodbye world/. Finally, we’re using the eq matcher to compare our subject with the string Hello world!. Let’s see what the output of this test would look like:

Hello world!
  should say hello

Finished in 0.00203 seconds (files took 0.07591 seconds to load)
1 examples, 0 failures

Notice we did not use the be matcher in our example. This is because the be matcher tests object identity while the eq matcher tests equality. Here’s what the output would look like if we used the be matcher:

Hello world!
  should say hello (FAILED - 1)

Failures:

describe 'Hello world!' do
  1) Hello world! should say hello
     Failure/Error: expect(subject).to be 'Hello world!'

       expected #<String:70291920962560> => "Hello world!"
            got #<String:70291943850520> => "Hello world!"

       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       `expect(actual).to eq(expected)` if you don't care about
       object identity in this example.
     # ./spec/blog_examples/hello_world_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.01117 seconds (files took 0.07584 seconds to load)
1 examples, 1 failure

Failed examples:

rspec ./spec/blog_examples/hello_world_spec.rb:2 # Hello world! should say hello

As you can see, the be matcher actually compares the identity of each object instance, but this example uses two different instances of String. We’re really only interested in comparing the values of the String instances so we use the eq matcher.

Contexts

RSpec offers two primary methods of grouping tests together. We’ve already looked at one way, by creating example groups with the describe method. The other way is to use the context method. The context method is really just an alias for the describe method. The describe method is generally used to create example groups that describe an object, while the context method is used to create example groups that describe the state of an object that can vary. Here’s an example describing an Array object with multiple contexts:

describe Array do
  context 'with no elements' do
    let(:subject) { [] }
    it 'should be empty' do
      expect(subject.count).to eq 0
    end
  end

  context 'with elements' do
    let(:subject) { ['foo', 'bar', 'baz'] }
    it 'should not be empty' do
      expect(subject.count).not_to eq 0
    end
  end
end

Here we’ve grouped our examples by the class of object we’re testing (Array), but we’ve segmented our examples based on the context we’re looking to describe. Each context introduces a new scope. Examples grouped by context can have their own subjects, or they can inherit their subject from their parent. Here’s an example where we inherit our subject from our parent and test for different contexts:

describe Hash do
  let(:baz_val) { nil }
  let(:subject) do
    { :foo => 'bar', :baz => baz_val }
  end

  it 'should have the foo key set to bar' do
    expect(subject[:foo]).to eq 'bar'
  end

  it 'should have the baz key set to nil' do
    expect(subject[:baz]).to be nil
  end

  context 'with baz_val set to qux' do
    let(:baz_val) { 'qux' }
    it 'should have the baz key set to qux' do
      expect(subject[:baz]).to eq 'qux'
    end
  end

  context 'with baz_val set to quux' do
    let(:baz_val) { 'quux' }
    it 'should have the baz key set to quux' do
      expect(subject[:baz]).to eq 'quux'
    end
  end
end

Here we’re testing the same Hash object, but we’re testing that object under different contexts. We’re overriding the baz_val method inside our context blocks and ensuring that our object behaves the way we expect it to under different conditions. This allows us to test parts of our code that vary with context while avoiding duplicating our tests that focus on the parts of our code that remain stable. Segmenting our examples into context blocks makes our test code and output more readable. Here’s the output of the above example:

Hash
  should have the foo key set to bar
  should have the baz key set to nil
  with baz_val set to qux
    should have the baz key set to qux
  with baz_val set to quux
    should have the baz key set to quux

Finished in 0.00189 seconds (files took 0.10379 seconds to load)
4 examples, 0 failures

You can declare a context outside of a describe block using the shared_context method. A shared context is a context that can be shared across many example groups. It’s a way to describe common behavior without having to resort to duplicating code. We can break out the common behavior in our example above into a shared context:

shared_context 'foo should always be set to bar' do
  it 'should have the foo key set to bar' do
    expect(subject[:foo]).to eq 'bar'
  end
end

describe Hash do
  let(:baz_val) { nil }
  let(:subject) do
    { :foo => 'bar', :baz => baz_val }
  end

  include_context 'foo should always be set to bar'

  it 'should have the baz key set to nil' do
    expect(subject[:baz]).to be nil
  end

  context 'with baz_val set to qux' do
    let(:baz_val) { 'qux' }
    include_context 'foo should always be set to bar'
    it 'should have the baz key set to qux' do
      expect(subject[:baz]).to eq 'qux'
    end
  end

  context 'with baz_val set to quux' do
    let(:baz_val) { 'quux' }
    include_context 'foo should always be set to bar'
    it 'should have the baz key set to quux' do
      expect(subject[:baz]).to eq 'quux'
    end
  end
end

Since the foo key should always be set to bar, we can include this shared context to ensure that our Hash meets our expectations in all of our contexts. Here’s the output of that test:

Hash
  should have the foo key set to bar
  should have the baz key set to nil
  with baz_val set to qux
    should have the foo key set to bar
    should have the baz key set to qux
  with baz_val set to quux
    should have the foo key set to bar
    should have the baz key set to quux

Finished in 0.00221 seconds (files took 0.08721 seconds to load)
6 examples, 0 failures

Shared contexts are a powerful way to describe common behavior without resorting to boilerplate. Shared contexts can be stored in separate files and loaded using the require method at the beginning of your spec file.

Making the connection

This post focused mostly on RSpec itself. In a future post I’ll bring these elements together with some examples using rspec-puppet and serverspec.