Parts of an Expectation

When you see an expectation like the following one:

 expect​(deck.cards.count).to eq(52), ​'not playing with a full deck'

…you’ll notice several forms of punctuation in use: parentheses, dots, and whitespace. While there’s some variety here, the syntax consistently uses just a few simple parts:

  • A subject—the thing you’re testing—that is, an instance of a Ruby class

  • A matcher—an object that specifies what you expect to be true about the subject, and provides the pass/fail logic

  • (optionally) A custom failure message

These parts are held together with a bit of glue code: expect, together with either the to or not_to method.

images/expectation-parts.png

Let’s get a feel for how these parts work by trying them out in an IRB session:

 $ ​​irb

To use rspec-expectations, you have to require the library, and then include the RSpec::Matchers module. Normally, rspec-core does this for you, but when you use rspec-expectations in another context, you need to bring it in yourself:

 >>​ require ​'rspec/expectations'
 => true
 >>​ ​include​ RSpec::Matchers
 => Object

With that done, you can now create an expectation:

 >>​ ​expect​(1).to eq(1)
 => true
 >>​ ​expect​(1).to eq(2)
 RSpec::Expectations::ExpectationNotMetError:
 expected: 2
  got: 1
 
 (compared using ==)
 
  backtrace truncated

RSpec signals failure by raising an exception. Other test frameworks use a similar technique when an assertion fails.

Wrapping Your Subject With expect

Ruby begins evaluating your expectation at the expect method. We’ll start there, too. Type the following code into your IRB session:

 >>​ expect_one = ​expect​(1)
 => #<RSpec::Expectations::ExpectationTarget:0x007fb4eb83a818 @target=1>

Here, our subject is the number 1. We’ve wrapped it in the expect method to give ourselves a place to attach methods like to or not_to. In other words, the expect method wraps our object in a test-friendly adapter.

Using a Matcher

If expect wraps your object for testing, then the matcher actually performs the test. The matcher checks that the subject satisfies its criteria. Matchers can compare numbers, find patterns in text, examine deeply nested data structures, or perform any custom behavior you need.

The RSpec::Matchers module ships with built-in methods to create matchers. Here, we’ll use its eq method to create a matcher that only matches the number 1:

 >>​ be_one = eq(1)
 => #<RSpec::Matchers::BuiltIn::Eq:0x007fb4eb82dd98 @expected=1>

This matcher can’t do anything on its own; we still need to combine it with the subject we saw in the previous section.

Putting the Pieces Together

At this point, we have a subject, 1, that we’ve wrapped inside the expect method to make it testable. We also have a matcher, be_one. We can put them together using the to or not_to method:

 >>​ expect_one.to(be_one)
 => true
 >>​ expect_one.not_to(be_one)
 RSpec::Expectations::ExpectationNotMetError:
 expected: value != 1
  got: 1
 
 (compared using ==)
 
  backtrace truncated

The to method tries to match the subject (in this case, the integer 1) against the provided matcher. If there’s a match, the method returns true; if not, it bails with a detailed failure message.

The not_to method does the opposite: it fails if the subject does match. If you like to boldly split infinitives, you can also use to_not in place of not_to:

 >>​ ​expect​(1).not_to eq(2)
 => true
 >>​ ​expect​(1).to_not eq(2)
 => true

When you think of RSpec expectations as being just a couple of simple Ruby objects glued together, the syntax becomes clear. You’ll use parentheses with the expect method call, a dot to attach the to or not_to method, and a space leading up to the matcher.

When an Expectation Fails

When the code you’re testing does not behave as you expect, it’s essential to have good, detailed error information to quickly diagnose what’s going on. As you’ve seen, RSpec’s matchers provide helpful failure messages right out of the box. Sometimes, though, you need a little more detail.

For example, consider the following expectation:

 >>​ resp = Struct.new(​:status​, ​:body​).new(400, ​'unknown query param `sort`'​)
 => #<struct status=400, body="unknown query param `sort`">
 
 >>​ ​expect​(resp.status).to eq(200)
 RSpec::Expectations::ExpectationNotMetError:
 expected: 200
  got: 400
 
 (compared using ==)
 
  backtrace truncated

“Expected 200; got 400” is technically correct, but it does not provide enough information to understand why we got a 400 “Bad Request” response. The HTTP server is telling us what we did wrong in the response body, but that information is not included in the failure message. To include this extra information, you can pass an alternate failure message along with the matcher to to or not_to:

 >>​ ​expect​(resp.status).to eq(200), ​"Got a ​​#{​resp.status​}​​: ​​#{​resp.body​}​​"
 RSpec::Expectations::ExpectationNotMetError: Got a 400: unknown query param ↩
 `sort`
  backtrace truncated

If the failure message is expensive to generate (for example, scanning a large core dump file), you can pass in a callable object such as a Proc or a Method object. That way, you only pay the cost if the spec fails:

 >>​ ​expect​(resp.status).to eq(200), resp.method(​:body​)
 RSpec::Expectations::ExpectationNotMetError: unknown query param `sort`
  backtrace truncated

Clear Failure Messages Save You Time

images/aside-icons/info.png

We can’t tell you how many times we’ve seen “failed assertion, no message given” in legacy test suites. When you encounter a vague failure message like that, the best you can do is add a bunch of puts calls with debugging info and then rerun your tests.

A good error message tells you exactly what went wrong so that you can start fixing it right away. Time saved diagnosing failures can translate directly to lower project costs.

When the matcher’s default failure message doesn’t provide enough detail, a custom message may be just what you need. If you find yourself using the same message repeatedly, you can save time by writing your own matcher instead. We’ll show you how to do that in Chapter 12, Creating Custom Matchers.

RSpec Expectations vs. Traditional Assertions

It might feel like RSpec’s expectations have a lot of moving parts. If you’ve used traditional assertions in an xUnit testing framework such as Ruby’s built-in Minitest, you may be wondering if the added complexity is worth it. In fact, expectations and assertions are same basic concept, just with different emphases.

Assertions are simpler to explain than RSpec’s expectations—and simplicity is a good thing—but that doesn’t necessarily make one better than the other. RSpec’s complexity provides a number of advantages over simple assert methods.

  • Composability: Matchers are first-class objects that can be combined and used in flexible ways that simple assert methods can’t.

  • Negation: Matchers can be automatically negated by passing them to expect(object).not_to, with no need for you to write an assert_not_xyz or refute_xyz method to pair with assert_xyz.

  • Readability: We’ve chosen to use a syntax that, when read out loud, sounds like an English description of the outcome you expect.

  • More useful errors. For example, the expectation for the following collection of numbers:

     expect​([13, 2, 3, 99]).to all be_odd

    ...tells you exactly which item in the collection failed:

     expected [13, 2, 3, 99] to all be odd
     
      object at index 1 failed to match:
      expected `2.odd?` to return true, got false

    The equivalent xUnit-style assertion, assert [13, 2, 3, 99].all?(&:odd), merely reports Expected false to be truthy.

Though we love using expectations, we will be the first to say they are not right for every project. You can easily use RSpec with a less complex library such as Minitest’s assertions, as we demonstrated in Library Configuration.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset