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.
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.
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.
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.
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.
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.
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.
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.
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.