Your Turn

We’ve covered a lot of ground this chapter! From basic Ruby building blocks like strings and numbers, through deeply nested collections, to methods with side effects, you can find a matcher to suit your needs.

All these matchers built into RSpec are designed to help you do two things:

  • Express exactly how you want the code to behave, without being too strict or too lax

  • Get precise feedback when something breaks so that you can find exactly where the failure happened

It’s much more important to keep these two principles in mind than it is to memorize all the different matchers. As you try your hand at the following exercises, refer to Appendix 3, Matcher Cheat Sheet to get inspiration for different matchers to try.

Exercises

Since matchers help you diagnose failures, we want to show you how to get helpful failure messages by choosing the right matcher. We wrote the following exercises to fail on purpose. While you’re certainly welcome to fix the underlying issue in as many exercises as you like, please focus first on experimenting with different matchers.

Matching Phone Numbers

Create a new spec file with the following description for a class that matches phone numbers from a string and yields them to its caller, one by one:

 RSpec.describe PhoneNumberExtractor ​do
 let​(​:text​) ​do
  <<-EOS
  Melinda: (202) 555-0168
  Bob: 202-555-0199
  Sabina: (202) 555-0176
  EOS
 end
 
 it​ ​'yields phone numbers as it finds them'​ ​do
  yielded_numbers = []
  PhoneNumberExtractor.extract_from(text) ​do​ |number|
  yielded_numbers << number
 end
 
 expect​(yielded_numbers).to eq [
 '(202) 555-0168'​,
 '202-555-0199'​,
 '(202) 555-0175'
  ]
 end
 end

Here’s a partial implementation of the spec for you to add to the top of the file—not enough to pass, but enough to show that there’s room for improvement in our specs:

 class​ PhoneNumberExtractor
 def​ self.extract_from(text, &block)
 # Look for patterns like (###) ###-####
  text.scan(​/(d{3}) d{3}-d{4}/​, &block)
 end
 end

Run this file through RSpec. Now, take a look at the spec code. We had to do a lot of work to set up a separate collection for the yielded phone numbers, and then compare it afterward. Change this example to use a matcher better suited to how the extract_from method works. Notice how much simpler and clearer the spec is now.

Banner Year

The next two exercises will be in the same example group and will share the same implementation class. Let’s start with the spec, which describes a fictional public company (named after a river) that had a good year. Add the following code to a new file:

 RSpec.describe PublicCompany ​do
 let​(​:company​) { PublicCompany.new(​'Nile'​, 10, 100_000) }
 
 it​ ​'increases its market cap when it gets better than expected revenues'​ ​do
  before_market_cap = company.market_cap
  company.got_better_than_expected_revenues
  after_market_cap = company.market_cap
 
 expect​(after_market_cap - before_market_cap).to be >= 50_000
 end
 end

At the top of your file, add the following (not yet correct) implementation of the PublicCompany class:

 PublicCompany = Struct.new(​:name​, ​:value_per_share​, ​:share_count​) ​do
 def​ got_better_than_expected_revenues
  self.value_per_share *= rand(1.05..1.10)
 end
 
 def​ market_cap
  @market_cap ||= value_per_share * share_count
 end
 end

Run your spec, and look at the failure message: expected: >= 50000 / got: 0. It’s pretty terse and doesn’t really communicate the intent of the code.

Update the expectation to describe how the code should behave, rather than what the value of a variable is.

About Our Company

We also want to check that our class is correctly storing all the information investors will want to know about the company. Add the following example inside the example group from the previous exercise, just after the other example:

 it​ ​'provides attributes'​ ​do
 expect​(company.name).to eq(​'Nil'​)
 expect​(company.value_per_share).to eq(10)
 expect​(company.share_count).to eq(10_000)
 expect​(company.market_cap).to eq(1_000_000)
 end

When you run this new example, RSpec stops the test at the first failure, on company.name. We don’t get to see whether or not any of the other attributes were correct.

Use a different matcher here that checks all the attributes, and reports on any differences between what we’re expecting and how the code actually behaves.

Tokenizing Words

For this exercise, we’re testing a tokenizer that breaks text into individual words. Add the following spec to a new file:

 RSpec.describe Tokenizer ​do
 let​(​:text​) ​do
  <<-EOS
  I am Sam.
  Sam I am.
  Do you like green eggs and ham?
  EOS
 end
 
 it​ ​'tokenizes multiple lines of text'​ ​do
  tokenized = Tokenizer.tokenize(text)
 expect​(tokenized.first(6)).to eq [​'I'​, ​'am'​, ​'Sam.'​, ​'Sam'​, ​'I'​, ​'am'​]
 end
 end

Add the following incorrect implementation of the Tokenizer class to the top of your new file:

 class​ Tokenizer
 def​ self.tokenize(string)
  string.split(​/ +/​)
 end
 end

Run the spec, and read the failure message. Our spec caught the error, but it didn’t give any context beyond just the six words we asked for. Moreover, if we ever update this spec, we have to take extra care to keep the length parameter, first(6), in sync with the list of expected words.

Change your spec to use a more future-proof matcher that doesn’t require us to extract a hard-coded number of tokens.

Building Blocks of Nature

For this example, we’ll be tearing apart the molecules making up our world around us. Fortunately, it’s just a simulation. Create a new file with the following spec for water:

 RSpec.describe Water ​do
 it​ ​'is H2O'​ ​do
 expect​(Water.elements.sort).to eq [​:hydrogen​, ​:hydrogen​, ​:oxygen​]
 end
 end

At the top of your file, add an implementation of Water that’s missing one of its hydrogen atoms:

 class​ Water
 def​ self.elements
  [​:oxygen​, ​:hydrogen​]
 end
 end

Run your spec. It will fail correctly, but the output leaves something to be desired. We just get two collections dumped to the console, and it’s up to us to read them by hand and find out what’s different. With just a few items, comparing by hand is manageable, but differences become much harder to spot as the collections get bigger.

Also, take a look at that sort call we had to add. This spec has nothing to do with sorting, but we had to sort the collection to ensure we were just comparing the elements without regard to order.

Fix our mistake here and use a matcher whose failure message clearly spells out the difference between the two collections.

Working for the Weekend

For our final example, we’re going to write a calendar-related spec that determines whether any given day is on the weekend:

 RSpec.describe Calendar ​do
 let​(​:sunday_date​) { Calendar.new(​'Sun, 11 Jun 2017'​) }
 
 it​ ​'considers sundays to be on the weekend'​ ​do
 expect​(sunday_date.on_weekend?).to be ​true
 end
 end

Here’s an obviously incorrect implementation of the on_weekend? method:

 require ​'date'
 
 Calendar = Struct.new(​:date_string​) ​do
 def​ on_weekend?
  Date.parse(date_string).saturday?
 end
 end

When you run this spec, you get the stilted phrase “to be true” in the output. Change this matcher to one that reads more clearly in the test report.

Bonus Points

As we mentioned earlier, the main point of these exercises was to practice using matchers that express just what you want to say about your code’s behavior (and no more), and that give you clear output.

So, there’s no need to fix the underlying implementations. But for extra credit, feel free to make the specs pass. Post your solutions in the forums, and we’ll send you a GIF of a gold star.

Either way, meet us in the next chapter to see how you can create your own matchers that are just as expressive as the built-in ones.

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

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