Generated Example Descriptions

Matchers have another useful ability over simpler assert methods: they’re self-describing. The matcher protocol includes the optional (but recommended) description method. All built-in matchers define this method:

 >>​ start_with(1).description
 => "start with 1"
 >>​ (start_with(1) & end_with(9)).description
 => "start with 1 and end with 9"
 >>​ contain_exactly( a_string_starting_with(1) & ending_with(9) ).description
 => "contain exactly (a string starting with 1 and ending with 9)"

As you can see, the descriptions of composed and compound matchers include the description of each part. These descriptions are used in failure messages when you pass one matcher to another. They can also help you reduce duplication in your specs. Typically, each example has a string stating the intended behavior:

 class​ CookieRecipe
 attr_reader​ ​:ingredients
 
 def​ initialize
  @ingredients = [​:butter​, ​:milk​, ​:flour​, ​:sugar​, ​:eggs​, ​:chocolate_chips​]
 end
 end
 
 RSpec.describe CookieRecipe, ​'#ingredients'​ ​do
 it​ ​'should include :butter, :milk and :eggs'​ ​do
 expect​(CookieRecipe.new.ingredients).to ​include​(​:butter​, ​:milk​, ​:eggs​)
 end
 
 it​ ​'should not include :fish_oil'​ ​do
 expect​(CookieRecipe.new.ingredients).not_to ​include​(​:fish_oil​)
 end
 end

These appear in the output when you run your specs with the documentation formatter:

 $ ​​rspec spec/cookie_recipe_spec.rb --format documentation
 
 CookieRecipe#ingredients
  should include :butter, :milk and :eggs
  should not include :fish_oil
 
 Finished in 0.00197 seconds (files took 0.08433 seconds to load)
 2 examples, 0 failures

Notice the duplication in the specs, though. We’ve explained each example twice: once in the description, and once in the code. If we remove the former, RSpec will write its own description based on the code:

 RSpec.describe CookieRecipe, ​'#ingredients'​ ​do
 specify​ ​do
 expect​(CookieRecipe.new.ingredients).to ​include​(​:butter​, ​:milk​, ​:eggs​)
 end
 
 specify​ ​do
 expect​(CookieRecipe.new.ingredients).not_to ​include​(​:fish_oil​)
 end
 end

We’ve also switched to the specify alias to avoid the grammatically awkward phrase it expect. When we run this version of the specs:

 $ ​​rspec spec/cookie_recipe_no_doc_strings_spec.rb --format documentation
 
 CookieRecipe#ingredients
  should include :butter, :milk, and :eggs
  should not include :fish_oil
 
 Finished in 0.00212 seconds (files took 0.08655 seconds to load)
 2 examples, 0 failures

...the output is exactly the same as when we wrote the descriptions by hand. This way, though, the specs are a litttle more future-proof. If we want to change an example later—to make the recipe dairy-free, for instance—we don’t have to worry about keeping our English description in sync with the code.

To generate these descriptions, RSpec takes the description of the last-executed expectation and prefixes it with should or should not.

We can simplify our specs even further using the subject method from rspec-core instead of repeating the recipe-creation code. This construct is the equivalent of calling let(:subject) { ... }:

 RSpec.describe CookieRecipe, ​'#ingredients'​ ​do
  subject { CookieRecipe.new.ingredients }
 it​ { is_expected.to ​include​(​:butter​, ​:milk​, ​:eggs​) }
 it​ { is_expected.not_to ​include​(​:fish_oil​) }
 end

The subject method defines how to build the object we’re testing. RSpec gives us is_expected as shorthand for expect(subject). The phrase it is_expected reads nicely in each spec here, though in larger projects we tend to favor more explicit let constructs for clarity’s sake.

Once again, we get the same documentation output:

 $ ​​rspec spec/cookie_recipe_subject_spec.rb --format documentation
 
 CookieRecipe#ingredients
  should include :butter, :milk, and :eggs
  should not include :fish_oil
 
 Finished in 0.00225 seconds (files took 0.08616 seconds to load)
 2 examples, 0 failures

Here’s an alternate form of this one-liner syntax that looks even more like the generated descriptions:

 RSpec.describe CookieRecipe, ​'#ingredients'​ ​do
  subject { CookieRecipe.new.ingredients }
 it​ { should ​include​(​:butter​, ​:milk​, ​:eggs​) }
 it​ { should_not ​include​(​:fish_oil​) }
 end

Wait, didn’t we just say should was problematic in What Happened to should?? The problem with the old should from RSpec 2 and before isn’t the name; it’s the fact that RSpec had to monkey-patch the core Object class to implement it.

This should is different: it’s just a local alias for expect(subject).to. You can use either should or is_expected.to, whichever you prefer.

We recommend you use this one-liner syntax sparingly. It’s possible to over-emphasize brevity and rely too much on one-liners. To keep our enthusiasm in check, let’s go over a few of the downsides of generated descriptions:

  • Since they’re not available until runtime, you can’t use them with the --example option to run a single spec.

  • Your specs’ output can be misleading if you change your setup code and forget to update the describe or context documentation.

  • Specs written in a one-liner style carry extra cognitive load; you have to understand how subject and is_expected/should relate.

The one case when we do recommend one-liner syntax is when the generated description is a near-exact duplicate of what you would have written by hand. For a good example, see the specs for the Mustermann string-matching library.[79]

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

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