Category Archives: Ruby

Don’t wrap instance variables in attr_reader unless necessary

I’ve occasionally come across advice to wrap instance variables in attr_reader. There are two supposed benefits of this practice, which I’ll describe shortly. First I’ll provide a piece of example code.

Example of wrapping instance variables in attr_reader

Original version

Here’s a tiny class that has just one instance variable, @name. You can see that @name is used in the loud_name method.

class User
  def initialize(name)
    @name = name
  end

  def loud_name
    "#{@name.upcase}!!!"
  end
end

attr_reader version

Here’s that same class with an attr_reader added. Notice how loud_name now references name rather than @name.

class User
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def loud_name
    "#{name.upcase}!!!"
  end
end

The purported benefits

Advocates of this technique seem to find two certain benefits in it. (I’ve also come across other supported benefits but I don’t find them strong enough to merit mentioning.)

It makes refactoring easier

Rationale: If you ever want to change @name from being defined by an instance variable to being defined by a method, then you don’t have to go changing all the instances of @name to name.

This reasoning isn’t wrong but it is weak. First, it’s very rare that a value will start its life as an instance variable and then at some later point need to change to a method. This has happened to me so few times that I can’t recall a single instance of it happening.

Second, the refactoring work that the attr_reader saves is only a trivial amount of work. The cost of skipping the attr_reader is that you have to e.g. change a handful of instances of @name, in one file, to name. Considering that this is a tiny amount of work and that it needs to happen perhaps once every couple years per developer, this justification seems very weak.

It saves you from typo failures

Rationale: If you’re using instance variables and you accidentally type @nme instead of @name, @nme will just return nil rather than raising an error. If you’re using attr_reader and you accidentally type nme instead of name, nme will in fact raise an error.

This justification is also true, but also weak. If typo-ing an instance variable allows a bug to silently enter your application, then your application is not tested well enough.

I would be in favor of saving myself from the typo problem if the attr_reader technique hardly cost anything to use, but as we’ll see shortly, the attr_reader technique’s cost is too high to justify its benefit. Since the benefit is so tiny, the cost would have to be almost nothing, which it’s not.

Reasons why the attr_reader technique is a bad idea

Adding a public attr_reader throws away the benefits of encapsulation

Private instance variables are useful for the same reason as private methods: because you know they’re not depended on by outside clients.

If I have a class that has an instance variable called @price, I know that I can rename that instance variable to @cost or change it to @price_cents (changing the whole meaning of the value) or even kill @price altogether. What I want to do with @price is 100% my business. This is great.

But if I add attr_reader :price to my class, my class suddenly has responsibilities. I can no longer be sure that the class where @price is defined is the only thing that depends on @price. Other clients throughout my application may be referring to @price. I’m no longer free to do away with @price or change its meaning. This makes my code riskier and harder to change.

You can add a private attr_reader, but that’s unnatural

If you want to make use of the attr_reader technique but you don’t want to throw away the benefits of encapsulation, you can add a private attr_reader. Here’s what that would look like.

# attr_reader version

class User
  def initialize(name)
    @name = name
  end

  def loud_name
    "#{name.upcase}!!!"
  end

  private

  attr_reader :name
end

This solves the encapsulation problem, but what have we really gained on balance? In exchange for not having to change @name to name on the off chance that we change name from an instance variable to a method, we have to pay the price of having this weird private attr_reader :name thing at the bottom of our class.

And consider that we would have to do this on every single class that has at least one instance variable!

Don’t wrap instance variables in attr_reader

Wrapping your instance variables in a public attr_reader changes your instance variables from private to public, increasing the public surface area of your class’s API and making your application a little bit harder to understand.

Wrapping your instance variables in a private attr_reader adds an unnatural piece of boilerplate to all your Ruby classes.

Given the tiny and dubious benefits that the attr_reader technique provides, this cost isn’t worth it.

Using attr_reader is good and necessary for values that really need to be public. As a default policy, wrapping instance variables in attr_reader is a bad idea.

Understanding Ruby blocks

Blocks are a fundamental concept in Ruby. Many common Ruby methods use blocks. Blocks are also an integral part of many domain-specific languages (DSLs) in libraries like RSpec, Factory Bot, and Rails itself.

In this post we’ll discuss what a block is. Then we’ll take a look at four different native Ruby methods that take blocks (times, each, map and tap) in order to better understand what use cases blocks are good for.

Lastly, we’ll see how to define our own custom method that takes a block.

What a block is

Virtually all languages have a way for functions to take arguments. You pass data into a function and then the function does something with that data.

A block takes that idea to a new level. A block is a way of passing behavior rather than data to a method. The examples that follow will illustrate exactly what is meant by this.

Native Ruby methods that take blocks

Here are four native Ruby methods that take blocks. For each one I’ll give a description of the method, show an example of the method being used, and then show the output that that example would generate.

Remember that blocks are a way to pass behavior rather than data into methods. In each description, I’ll use the phrase “Behavior X” to describe the behavior that might be passed to the method.

Method: times

Description: “However many times I specify, repeat Behavior X.”

Example: three times, print the text “hello”. (Behavior X is printing “hello”.)

3.times do
  puts "hello"
end

Output:

hello
hello
hello

Method: each

“Take this array. For each element in the array, execute Behavior X.”

Example: iterate over an array containing three elements and print each element. (Behavior X is printing the element.)

[1, 2, 3].each do |n|
  puts n
end

Output:

1
2
3

Method: map

“Take this array. For each element in the array, execute Behavior X, append the return value of X to a new array, and then after all the iterations are complete, return the newly-created array.”

Example: iterate over an array and square each element. (Behavior X is squaring the element.)

squares = [1, 2, 3].map do |n|
  n * n
end

puts squares.join(",")

Output:

1,4,9

Method: tap

“See this value? Perform Behavior X and then return that value.”

Example: initialize a file, write some content to it, then return the original file. (Behavior X is writing to the file.)

require "tempfile"

file = Tempfile.new.tap do |f|
  f.write("hello world")
  f.rewind
end

puts file.read

Output:

hello world

Now let’s look at how we can write our own method that can take a block.

Custom methods that take blocks

An HTML generator

Here’s a method which we can give an HTML tag as well as a piece of behavior. The method will execute our behavior. Before and after the behavior will be the opening and closing HTML tags.

inside_tag("p") do
  puts "Hello"
  puts "How are you?"
end

The output of this code looks like this.

<p>
Hello
How are you?
</p>

In this example, the “Behavior X” that we’re passing to our method is printing the text “Hello” and then “How are you?”.

The method definition

Here’s what the definition of such a method might look like.

def inside_tag(tag, &block)
  puts "<#{tag}>"  # output the opening tag
  block.call       # call the block that we were passed
  puts "</#{tag}>" # output the closing tag
end

Adding an argument to the block

Blocks can get more interesting when add arguments.

In the below example, the inside_tag block now passes an instance of Tag back to the block, allowing the behavior in the block to call tag.content rather than just puts. This allows our content to be indented.

class Tag
  def content(value)
    puts "  #{value}"
  end
end

def inside_tag(tag, &block)
  puts "<#{tag}>"
  block.call(Tag.new)
  puts "</#{tag}>"
end

inside_tag("p") do |tag|
  tag.content "Hello"
  tag.content "How are you?"
end

The above code gives the following output.

<p>
  Hello
  How are you?
</p>

Passing an object back to a block is a common DSL technique used in libraries like RSpec, Factory Bot, and Rails itself.

The technical details of blocks

There are a lot of technical details to learn about blocks. There are some interesting questions you could ask about blocks, including the following:

These are all good questions worth knowing the answer to, and you can click the links above to find out. But understanding these details is not necessary in order to understand the high-level gist of blocks.

Takeaway

A block is a way of passing behavior rather than data to a method. Not only do native Ruby methods make liberal use of blocks, but so do many popular Ruby libraries. Custom methods that take blocks can also sometimes be a good way to add expressiveness to your own applications.

Beware of “service objects” in Rails

Note: I no longer endorse this post, which was a rough first attempt to convey my thoughts on service objects. For my newer take on service objects, see my other post, How I code without service objects.

The good and bad thing about Active Record models

In the early stages of a Rails project’s life, the pattern of putting all the model code into objects that inherit from Active Record works out pretty nicely. Each model object gets its own database table, some validations, some associations, and a few custom methods.

Later in the project’s lifecycle, this pattern of putting everything into Active Record objects gets less good. If there’s an Appointment model, for example, everything remotely related to an appointment gets put into the Appointment model, leading to models with tens of methods and hundreds if not thousands of lines of code.

Despite the fact that this style of coding—stuffing huge amounts of code into Active Record models—leads to a mess, many Rails projects are built this way. My guess is that the reason this happens is that developers see the organizational structures Rails provides (models, controllers, views, helpers, etc.) and don’t realize that they’re not limited to ONLY these structures. I myself for a long time didn’t realize that I wasn’t limited to only those structures.

Service objects as an alternative to the “Active Record grab bag” anti-pattern

A tactic I frequently hear recommended as an antidote to the “Active Record grab bag” anti-pattern is to use “service objects”. I put the term “service objects” in quotes because it seems to mean different things to different people.

For my purposes I’ll use the definition that I’ve been able to synthesize from several of the top posts I found when I googled for rails service objects.

The idea is this: instead of putting domain logic in Active Record objects, you put domain logic in service objects which live in a separate place in your Rails application, perhaps in app/services. Some example service class names I found in various service object posts I found online include:

  • TweetCreator
  • RegisterUser
  • CompleteOrder
  • NewRegistrationService

So, typically, a service object is responsible for carrying out some action. A TweetCreator creates a tweet, a RegisterUser object registers a user. This seems to be the most commonly held (or at least commonly written about) conception of a service object. It’s also apparently the idea behind the Interactor gem.

Why service objects are a bad idea

One of the great benefits of object-oriented programming is that we can bestow objects with a mix of behavior and data to give the objects powerful capabilities. Not only this, but we can map objects fairly neatly with concepts in the domain model in which we’re working, making the code more easily understandable by humans.

Service objects throw out the fundamental advantages of object-oriented programming.

Instead of having behavior and data neatly encapsulated in easy-to-understand objects with names like Tweet or User, we have conceptually dubious ideas like TweetCreator and RegisterUser. “Objects” like this aren’t abstractions of concepts in the domain model. They’re chunks of procedural code masquerading as object-oriented code.

A better alternative to service objects: domain objects

Let me take a couple service object examples I’ve found online and rework them into something better.

Tweet example

The first example I’ll use is TweetCreator from this TopTal article, the first result when I google for rails service objects.

class TweetCreator
  def initialize(message)
    @message = message
  end

  def send_tweet
    client = Twitter::REST::Client.new do |config|
      config.consumer_key        = ENV['TWITTER_CONSUMER_KEY']
      config.consumer_secret     = ENV['TWITTER_CONSUMER_SECRET']
      config.access_token        = ENV['TWITTER_ACCESS_TOKEN']
      config.access_token_secret = ENV['TWITTER_ACCESS_SECRET']
    end
    client.update(@message)
  end
end

I think it’s far better just to have a Tweet object.

class Tweet
  def initialize(message)
    @message = message
  end

  def deliver
    client = Twitter::REST::Client.new do |config|
      config.consumer_key        = ENV['TWITTER_CONSUMER_KEY']
      config.consumer_secret     = ENV['TWITTER_CONSUMER_SECRET']
      config.access_token        = ENV['TWITTER_ACCESS_TOKEN']
      config.access_token_secret = ENV['TWITTER_ACCESS_SECRET']
    end

    client.update(@message)
  end
end

Isn’t it more natural to say Tweet.new('hello').deliver than to say TweetCreator.new('hi').send_tweet? I think so. Rather than being this weird single-purpose procedure-carrier-outer, Tweet is just a simple representation of a real domain concept. This, to me, is what domain objects are all about.

The differences between the good and bad examples in this case are pretty small, so let me address the next example in the TopTal article which I think is worse.

Currency exchange example

module MoneyManager
  # exchange currency from one amount to another
  class CurrencyExchanger < ApplicationService
    ...
    def call
      ActiveRecord::Base.transaction do
        # transfer the original currency to the exchange's account
        outgoing_tx = CurrencyTransferrer.call(
          from: the_user_account,
          to: the_exchange_account,
          amount: the_amount,
          currency: original_currency
        )

        # get the exchange rate
        rate = ExchangeRateGetter.call(
          from: original_currency,
          to: new_currency
        )

        # transfer the new currency back to the user's account
        incoming_tx = CurrencyTransferrer.call(
          from: the_exchange_account,
          to: the_user_account,
          amount: the_amount * rate,
          currency: new_currency
        )

        # record the exchange happening
        ExchangeRecorder.call(
          outgoing_tx: outgoing_tx,
          incoming_tx: incoming_tx
        )
      end
    end
  end

  # record the transfer of money from one account to another in money_accounts
  class CurrencyTransferrer < ApplicationService
    ...
  end

  # record an exchange event in the money_exchanges table
  class ExchangeRecorder < ApplicationService
    ...
  end

  # get the exchange rate from an API
  class ExchangeRateGetter < ApplicationService
    ...
  end
end

First, the abstractions of MoneyManager, CurrencyExchanger, etc. aren’t really abstractions. I’m automatically skeptical of any object whose name ends in -er.

I’m not going to try to rework this example line for line because there’s too much there, but let’s see if we can start toward something better.

class CurrencyValue
  def initialize(amount_cents, currency_type)
    @amount_cents = amount_cents
    @currency_type = currency_type
  end

  def converted_to(other_currency_type)
    exchange_rate = ExchangeRate.find(@currency_type, other_currency_type)
    CurrencyValue.new(@amount_cents * exchange_rate, other_currency_type)
  end
end

one_dollar = CurrencyValue.new(100, CurrencyType.find('USD'))
puts one_dollar.converted_to(CurrencyType.find('GBP')) # 0.80

Someone could probably legitimately find fault with the details of my currency conversion logic (an area with which I have no experience) but hopefully the conceptual superiority of my approach over the MoneyManager approach is clear. A currency value is clearly a real thing in the real world, and so is a currency type and so is an exchange rate. Things like MoneyManager, CurrencyExchanger and ExchangeRateGetter are clearly just contrived. These latter objects (which again are really just collections or procedural code) would probably fall under the category of what Martin Fowler calls an anemic domain model.

Suggestions for further reading

Enough With the Service Objects Already

I really enjoyed and agreed with Avdi Grimm’s Enough With the Service Objects Already. Avdi’s post helped me realize that most service objects are just wrappers for chunks of procedural code. What I wanted to add was that I think it’s usually possible to refactor that procedural code into meaningful objects. For example, in a piece of schedule code I recently wrote, I have a concept of an AvailabilityBlock and a mechanism for detecting conflicts between them. Instead of taking the maybe “obvious” route of creating a AvailabilityBlockConflictDetector, I created an object called an AvailabilityBlockPair which can be used like this: AvailabilityBlockPair.new(a, b).conflict?. To me this is much nicer. The concept of an AvailabilityBlockPair isn’t something that obviously exists in the domain, but it does exist in the domain if I consider it to be. It’s like drawing an arbitrary border around letters in a word search. Any word you can find on the page is really there if you can circle it.

Anemic Domain Model

Martin Fowler’s Anemic Domain Model post helped me articulate exactly what’s wrong with service objects, which seem to me to be a specific kind of Anemic Domain Model. My favorite passage from the article is: “The fundamental horror of this anti-pattern [Anemic Domain Model] is that it’s so contrary to the basic idea of object-oriented design; which is to combine data and process together. The anemic domain model is really just a procedural style design, exactly the kind of thing that object bigots like me (and Eric) have been fighting since our early days in Smalltalk. What’s worse, many people think that anemic objects are real objects, and thus completely miss the point of what object-oriented design is all about.”

Martin Fowler on Service Objects via the Ruby Rogues Parley mailing list

This GitHub Gist contains what’s supposedly an excerpt from an email Martin Fowler wrote. This email snippet helped clue me into the apparent fact that what most people call service objects are really just an implementation of the Command pattern. It seems to me that the Interactor gem is also an implementation of the Command pattern. I could see the Command pattern making sense in certain scenarios but I think a) when the Command pattern is used it should be called a Command and not a service object, and b) it’s not a good go-to pattern for all behavior in an application. I’ve seen engineering teams try to switch over big parts of their application to Interactors, thinking it’s a great default style of coding. I’m highly skeptical that this is the case. I want to write object-oriented code in Ruby, not procedural code in Interactor-script.

Don’t Create Objects That End With -ER

If an object ends in “-er” (e.g. Manager, Processor), chances are it’s not a valid abstraction. There’s probably a much more fitting domain object in there (or aggregate of domain objects) if you think hard enough.

The Devise gem code

I think the Devise gem does a pretty good job of finding non-obvious domain concepts and turning them into sensible domain objects. I’ve found it profitable to study the code in that gem.

Domain-Driven Design

I’m slowly trudging through this book at home as I write this post. I find the book a little boring and hard to get through but I’ve found the effort to be worth it.

Summary/takeaway

I find the last sentence of Martin Fowler’s Anemic Domain Model article to be a great summary of what I’m trying to convey: “In general, the more behavior you find in the services, the more likely you are to be robbing yourself of the benefits of a domain model. If all your logic is in services, you’ve robbed yourself blind.”

Don’t use service objects. Use domain objects instead.

Why I recommend against using Cucumber

Around the time I first started using Rails in 2011, I noticed that a lot of developers, seemingly all Rails developers, were using Cucumber to assist with testing.

I bought into the idea—describing test cases in plain English—but in practice I found Cucumber not to be very valuable. In fact, my experience has been that Cucumber adds a negative amount of value.

In recent years I’ve noticed (although this is anecdotal and might just be my perception) that fewer codebases seem to use Cucumber and that fewer Rails developers seem to be on board with Cucumber. I had thought Cucumber was pretty much dead. But lately, to my surpise, I’ve seen Cucumber recommended to testing noobs more than a few times. Since I consider Cucumber to be a bad thing, I want to explain why I think so and why I don’t think other people should use it.

In my view there are two general ways Cucumber can be used: it can be used as intended or it can be abused. In the former case, I believe Cucumber has a small negative value. In the latter case I believe it has a large negative value.

Why Cucumber is bad when it’s not used as intended

Most production Cucumber scenarios I’ve seen look something like this:

Given a user exists with email "test@example.com" and password "mypassword"
And I visit "/sign_in"
And I fill in the "Email" field with "test@example.com"
And I fill in the "Password" field with "mypassword"
And I click "Sign In"
And I visit "/user/edit"
And I fill in the "First Name" field with "John"
And I fill in the "Last Name" field with "Smith"
And I fill in the "Age" field with "30"
And I click "Save"
And I visit "/profile"
Then I should see "John Smith, 30"

These kinds of tests, with fine-grained steps, arise when the developers seem to mistake Cucumber for a way to write Ruby in English. The above scenario provides exactly zero benefit, in my opinion, over the following equivalent Capybara scenario:

FactoryBot.create(:user, email: 'test@example.com', password: 'mypassword')

visit sign_in_path
fill_in 'Email', with: 'test@example.com'
fill_in 'Password', with: 'mypassword'
click_on 'Sign In'

visit edit_user_path
fill_in 'First Name', with: 'John'
fill_in 'Last Name', with: 'Smith'
fill_in 'Age', with: '30'
click_on 'Save'

visit profile_path
expect(page).to have_content('John Smith, 30')

The Cucumber/Gherkin version is no shorter nor more easily understandable.

To be fair to Cucumber, nobody who understands Cucumber advocates writing Cucumber scenarios in this way. The Cucumber creator himself, Aslak Hellesøy, wrote a post in 2011 saying not to do this. Other people have written similar things.

I think it’s telling that so many people have written blog posts advising against the very common practice of writing fine-grained Cucumber steps. To me it’s kind one of those gas station doors that looks for all the world like a pull door, so every single person who comes up to it pulls it instead of pushes it, feels like a dumbass, and then pushes it. So the gas station manager puts up a big sign that says “PUSH”, but most people don’t notice it and the problem persists. What instead should have been done is to make the push door look like a push door, without the big useless handle that you’re not supposed to pull. I get that the Cucumber maintainers tried to do that by removing `web_steps.rb`, but in my experience it didn’t seem to work.

And it doesn’t matter much anyway because Cucumber still sucks even if you don’t abuse it by writing fine-grained steps. I’ll explain why I think so.

Why Cucumber is bad even when it is used as intended

Here’s a version of the above Cucumber scenario that’s done in the way the Cucumber creators would intend. There are two parts.

First, the Gherkin steps:

Given I am signed in
And I provide my name and age details
Then I should see those details on my profile page

Second, the underlying Ruby steps:

Given /^I am signed in$/ do
  visit sign_in_path
  fill_in 'Email', with: 'test@example.com'
  fill_in 'Password', with: 'mypassword'
  click_on 'Sign In'
end

And /^I provide my name and age details$/ do
  visit edit_user_path
  fill_in 'First Name', with: 'John'
  fill_in 'Last Name', with: 'Smith'
  fill_in 'Age', with: '30'
  click_on 'Save'
end

Then /^I should see those details on my profile page$/ do
  visit profile_path
  expect(page).to have_content('John Smith, 30')
end

This is actually pretty decent-looking and appealing, at least at first glance. There are two problems, though. First, this way of doing things doesn’t really provide any clarity over doing it the Capybara way. Second, the step definitions usually end up in a single, flat file full of “step soup” where unrelated steps are mixed together willy-nilly.

Compare this again with the Capybara version:

FactoryBot.create(:user, email: 'test@example.com', password: 'mypassword')

visit sign_in_path
fill_in 'Email', with: 'test@example.com'
fill_in 'Password', with: 'mypassword'
click_on 'Sign In'

visit edit_user_path
fill_in 'First Name', with: 'John'
fill_in 'Last Name', with: 'Smith'
fill_in 'Age', with: '30'
click_on 'Save'

visit profile_path
expect(page).to have_content('John Smith, 30')

The sign in portion is usually abstracted away in Capybara, too, so the scenario would look more like this:

FactoryBot.create(:user, email: 'test@example.com', password: 'mypassword')

sign_in

visit edit_user_path
fill_in 'First Name', with: 'John'
fill_in 'Last Name', with: 'Smith'
fill_in 'Age', with: '30'
click_on 'Save'

visit profile_path
expect(page).to have_content('John Smith, 30')

That’s not too crazy at all. In order for Cucumber to be a superior solution to using bare Capybara, it would have to have some pretty strong benefits to compensate for the maintenance burden and cognitive overhead it adds. But it doesn’t.

So what do I recommend doing instead of using Cucumber? I think just using Capybara by itself is fine, and better than using Capybara + Cucumber. I also think Capybara + page objects is a pretty good way to go.

Rails testing resource roundup

.resource-item {
border-bottom: 1px solid #DDD;
padding-bottom: 50px;
margin-top: 30px;
margin-bottom: 50px;
}

Below is a list of testing resources I’ve either used myself or heard recommended. I tried to make the list as Ruby-centric as possible, although some resources from other ecosystems are so good that I didn’t want to exclude them.

I intend this to be a living list that grows over time. If you know of something that should be on this list but isn’t, please let me know, either in a comment on this page or via email.

Disclosure statement: None of the links below are affiliate links, although I do have a relationship with some of the authors/creators of these resources.

Section One: Ruby/Rails Specific Resources

Print Book: Rails 5 Test Prescriptions


Excerpt from Pragmatic Bookshelf summary:

“Does your Rails code suffer from bloat, brittleness, or inaccuracy? Cure these problems with the regular application of test-driven development. You’ll use Rails 5.2, Minitest 5, and RSpec 3.7, as well as popular testing libraries such as factory_bot and Cucumber.”

Details at The Pragmatic Bookshelf

Side note: you can also listen to my interview with author Noel Rappin on the Ruby Testing Podcast.

eBook: Everyday Rails Testing with RSpec


Summary from Leanpub:

“Real-world advice for adding reliable tests to your Rails apps with RSpec, complete with expanded, exclusive content and a full sample application. Updates for 2017 now available—RSpec 3.6, Rails 5.1, and more! Learn to test with confidence!”

Details at Leanpub

Screencast Series: Destroy All Software

Not everything on Destroy All Software (DAS) is testing-related but a lot of it is. I often see DAS recommended when people ask for testing-related resources.

Destroy All Software Catalog

Online Course Series: Upcase’s “Learn Testing” Courses

Summary from Upcase website:

“Test-driven development, or TDD, is the practice of writing your tests firsts, then using those tests to guide you as you write your actual production code. This may sound crazy, but it turns out that it makes writing code much easier. It provides a clear workflow and next steps while you’re building and has the added benefit of producing a test suite you can have confidence in. With these courses and videos we’ll teach you everything you need to know to get started with TDD.”

Details at Upcase

Print Book/eBook: Effective Testing with RSpec 3

Excerpt from Pragmatic Bookshelf summary:

“This definitive guide from RSpec’s lead developer shows you how to use RSpec to drive more maintainable designs, specify and document expected behavior, and prevent regressions during refactoring. Build a project using RSpec to design, describe, and test the behavior of your code. Whether you’re new to automated tests or have been using them for years, this book will help you write more effective tests.”

Details at The Pragmatic Bookshelf

Print Book/eBook: Practical Object-Oriented Design in Ruby

This isn’t specifically a testing book but I’ve seen it recommended a number of times as a book that will help you write better Ruby tests.

Excerpt from summary:
“[Practical Object-Oriented Design in Ruby] explains object-oriented design (OOD) using realistic, understandable examples. POODR* is a practical, readable introduction to how OOD can lower your costs and improve your applications.”

Details at Author’s Website

Section Two: Non-Rails-Specific Resources

Print Book: Growing Object-Oriented Software, Guided by Tests

This book is a classic in the testing world. I first read Growing Object-Oriented Software, Guided by Tests (GOOS) when I was clueless about testing. It helped me get oriented and learn what’s what. Among the most important concepts I learned from this book is the idea of a Walking Skeleton.

Details at Amazon

Print Book: Working Effectively with Legacy Code

I’ve worked on dozens of codebases so far in my career. Most of them have been legacy code. This book was super helpful in showing techniques like the Sprout Method technique to help get legacy code under control.

Details at Amazon

Taming legacy Ruby code using the “Sprout Method” technique (example 1)

Note: after I wrote this post I came up with a better example, which can be found here.

There are a number of reasons why it can often be difficult to add test coverage to legacy projects.

One barrier is the entanglement of dependencies that stands in the way of instantiating the objects you want to test.

Another barrier is the “wall of code” characteristic often present in legacy code: long, unstructured classes and methods with obfuscated variable names and mysterious behavior. Testing big things is a lot harder than testing small things.

In this example I’m going to demonstrate how to take a long, unstructured method, put tests on it, and then improve its design.

Legacy code example

Here’s a piece of crappy simulated legacy code:

require 'date'

class CreditCard
  def initialize(number, expiration_date, brand)
    @number = number
    @expiration_date = expiration_date
    @brand = brand
  end

  def valid?
    month, year = @expiration_date.split('/').map(&:to_i)
    year += 2000
    if DateTime.now.to_date < Date.new(year, month)
      return false
    else
      if @brand == 'American Express'
        @number.gsub(/\s+/, '').length == 15
      else
        @number.gsub(/\s+/, '').length == 16
      end
    end
  end
end

Why is this code crappy? For one, the `valid?` method is too long to easily be understood at a glance. Secondly, it has a bug. If you give the class an expired card it will still tell you the card is valid.

> cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
 => #<CreditCard:0x007fb8d9825890 @number="3843111122223333", @expiration_date="02/17", @brand="Visa"> 
> cc.valid?
 => true

If we want to fix this code there are a couple different approaches we could take.

Two Possible Approaches

  1. Try to debug the code by tediously running it over and over on the console
  2. Write some tests

The second approach has the benefit of being not only less annoying but also permanent. If we write a test to enforce that the expiration date bug is not present, we can reasonably expect that our test will protect against this bug forever. If we just debug the code in the console, we get no such guarantee.

But it’s not always easy to just go ahead and start writing tests.

Obstacles to Writing Tests

There’s a certain obstacle to testing that’s often present in legacy code. That is, when you have a line or file that’s a bazillion lines long, it’s hard to isolate the piece of code you’re interested in testing. There are often dependencies tangled up in the code. You’re afraid to try to untangle the dependencies because you don’t want to change code you don’t understand. But in order to understand the code, you need to untangle the dependencies. It’s a tough catch-22 type situation.

The Solution

The solution I like to apply to this problem is Michael Feathers’ sprout method technique which he describes in Working Effectively with Legacy Code.

What we’re about to do is:

  1. Write a test around the buggy area—expiration date validation—and watch it fail
  2. Extract the expiration date code into its own method so we can isolate the incorrect behavior
  3. Fix the bug and watch our test pass
  4. Let’s start by writing the first test.

    Writing the First Test

    I’ll write a test that says, “When the credit card’s expiration date is in the past, expect that card not to be valid.”

    require 'rspec'
    require './credit_card'
    
    describe CreditCard do
      describe '#valid?' do
        context 'expired' do
          it 'is not valid' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).not_to be_valid
          end
        end
      end
    end

    When I run this test it fails, as expected.

    Next I’ll break up this code a little bit by extracting a chunk of it into its own method.

    Extracting Our First Method

    The highlighted chunk below looks like a pretty good candidate to be separated.

    def valid?
      month, year = @expiration_date.split('/').map(&:to_i)
      year += 2000
      if DateTime.now.to_date < Date.new(year, month)
        return false
      else
        if @brand == 'American Express'
          @number.gsub(/\s+/, '').length == 15
        else
          @number.gsub(/\s+/, '').length == 16
        end
      end
    end

    I’m going to extract it into a `number_is_right_length?` method.

    require 'date'
    
    class CreditCard
      def initialize(number, expiration_date, brand)
        @number = number
        @expiration_date = expiration_date
        @brand = brand
      end
    
      def valid?
        month, year = @expiration_date.split('/').map(&:to_i)
        year += 2000
        if DateTime.now.to_date < Date.new(year, month)
          return false
        else
          number_is_right_length?
        end
      end
    
      def number_is_right_length?
        if @brand == 'American Express'
          @number.gsub(/\s+/, '').length == 15
        else
          @number.gsub(/\s+/, '').length == 16
        end
      end
    end

    Now that I have this functionality in its own method, I’ll write a test for just that part of the code, in isolation.

    require 'rspec'
    require './credit_card'
    
    describe CreditCard do
      describe '#valid?' do
        context 'expired' do
          it 'is not valid' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).not_to be_valid
          end
        end
      end
    
      describe '#number_is_right_length?' do
        context 'no spaces' do
          context 'right length' do
            it 'returns true' do
              cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
              expect(cc.number_is_right_length?).to be true
            end
          end
        end
      end
    end

    If I run this test it will pass.

    There’s something I don’t like about this, though. The expiration date of the card doesn’t have anything to do with the validity of the length of the card number. The extra setup data is distracting. A future maintainer might look at the test and assume that the expiration date is necessary.

    Removing Extraneous Setup Data

    It would be nice if we could test just the number length by itself. It would be nice if our test could look like this:

    require 'rspec'
    require './credit_card'
    
    describe CreditCard do
      describe '#valid?' do
        context 'expired' do
          it 'is not valid' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).not_to be_valid
          end
        end
      end
    
      describe '#number_is_right_length?' do
        context 'no spaces' do
          context 'right length' do
            it 'returns true' do
              valid = CreditCard.number_is_right_length?('Visa', '3843111122223333')
              expect(valid).to be true
            end
          end
        end
      end
    end

    In order to make our new `number_is_right_length?` method more testable in isolation, I’m going to change it from an instance method to a class method. Instead of using the `@brand` and `@number` instance variables, I’m going to use dependency injection so the `number_is_right_length?` doesn’t rely on a `CreditCard` instance having been created first.

    require 'date'
    
    class CreditCard
      def initialize(number, expiration_date, brand)
        @number = number
        @expiration_date = expiration_date
        @brand = brand
      end
    
      def valid?
        month, year = @expiration_date.split('/').map(&:to_i)
        year += 2000
        if DateTime.now.to_date < Date.new(year, month)
          return false
        else
          self.class.number_is_right_length?(@brand, @number)
        end
      end
    
      def self.number_is_right_length?(brand, number)
        if brand == 'American Express'
          number.gsub(/\s+/, '').length == 15
        else
          number.gsub(/\s+/, '').length == 16
        end
      end
    end

    Extracting Expiration-Related Code

    Now I’m going to extract the expiration-related code into its own method. That code is highlighted below.

    def valid?
      month, year = @expiration_date.split('/').map(&:to_i)
      year += 2000
      if DateTime.now.to_date < Date.new(year, month)
        return false
      else
        self.class.number_is_right_length?(@brand, @number)
      end
    end

    That chunk of code will go out of the `valid?` method and into a new `expired?` method.

    require 'date'
    
    class CreditCard
      def initialize(number, expiration_date, brand)
        @number = number
        @expiration_date = expiration_date
        @brand = brand
      end
    
      def valid?
        if expired?
          false
        else
          self.class.number_is_right_length?(@brand, @number)
        end
      end
    
      def expired?
        month, year = @expiration_date.split('/').map(&:to_i)
        year += 2000
        DateTime.now.to_date < Date.new(year, month)
      end
    
      def self.number_is_right_length?(brand, number)
        if brand == 'American Express'
          number.gsub(/\s+/, '').length == 15
        else
          number.gsub(/\s+/, '').length == 16
        end
      end
    end

    Similar to how I did with the `number_is_right_length?` method, I’ll now write a test for just the `expired?` method in isolation. The main reasons for extracting code into a smaller method are a) smaller methods are easier to understand, b) smaller methods can more easily be given descriptive and appropriate names, further aiding understanding, and c) it’s more manageable to write tests for small methods with few dependencies than large methods with many dependencies.

    Finding and Fixing the Bug

    This test will say, “When the expiration date is in the past, this method should return false.”

    require 'rspec'
    require './credit_card'
    
    describe CreditCard do
      describe '#valid?' do
        context 'expired' do
          it 'is not valid' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).not_to be_valid
          end
        end
      end
    
      describe '#number_is_right_length?' do
        context 'no spaces' do
          context 'right length' do
            it 'returns true' do
              valid = CreditCard.number_is_right_length?('Visa', '3843111122223333')
              expect(valid).to be true
            end
          end
        end
      end
    
      describe '#expired?' do
        context 'expired' do
          it 'returns true' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).to be_expired
          end
        end
      end
    end

    If I run this test I notice that it does not pass. I’ll write another test for the opposite scenario: when the expiration date is in the future, the method should return true.

    require 'rspec'
    require './credit_card'
    
    describe CreditCard do
      describe '#valid?' do
        context 'expired' do
          it 'is not valid' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).not_to be_valid
          end
        end
      end
    
      describe '#number_is_right_length?' do
        context 'no spaces' do
          context 'right length' do
            it 'returns true' do
              valid = CreditCard.number_is_right_length?('Visa', '3843111122223333')
              expect(valid).to be true
            end
          end
        end
      end
    
      describe '#expired?' do
        context 'expired' do
          it 'returns true' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).to be_expired
          end
        end
    
        context 'not expired' do
          it 'returns false' do
            cc = CreditCard.new('3843111122223333', '02/30', 'Visa')
            expect(cc).not_to be_expired
          end
        end
      end
    end

    This test also does not pass. So the method returns true when it should return false and false when it should return true. It’s now clear to me where the mistake lies: I put `<` when I should have put `>`!

    Switching from `<` to `>` fixes the bug and makes both tests pass.

    require 'date'
    
    class CreditCard
      def initialize(number, expiration_date, brand)
        @number = number
        @expiration_date = expiration_date
        @brand = brand
      end
    
      def valid?
        if expired?
          false
        else
          self.class.number_is_right_length?(@brand, @number)
        end
      end
    
      def expired?
        month, year = @expiration_date.split('/').map(&:to_i)
        year += 2000
        DateTime.now.to_date > Date.new(year, month)
      end
    
      def self.number_is_right_length?(brand, number)
        if brand == 'American Express'
          number.gsub(/\s+/, '').length == 15
        else
          number.gsub(/\s+/, '').length == 16
        end
      end
    end

    But again, I’m not a huge fan of the fact that I have to bring irrelevant setup data into the picture. Card number and brand have no bearing on whether a date has passed or not.

    I can give this method the same treatment as `number_is_right_length?` by making it a class method and adding an `expiration_date` parameter.

    require 'date'
    
    class CreditCard
      def initialize(number, expiration_date, brand)
        @number = number
        @expiration_date = expiration_date
        @brand = brand
      end
    
      def valid?
        if self.class.expired?(@expiration_date)
          false
        else
          self.class.number_is_right_length?(@brand, @number)
        end
      end
    
      def self.expired?(expiration_date)
        month, year = expiration_date.split('/').map(&:to_i)
        year += 2000
        DateTime.now.to_date > Date.new(year, month)
      end
    
      def self.number_is_right_length?(brand, number)
        if brand == 'American Express'
          number.gsub(/\s+/, '').length == 15
        else
          number.gsub(/\s+/, '').length == 16
        end
      end
    end

    Now I can change my `expired?` tests to only deal with date and nothing else.

    require 'rspec'
    require './credit_card'
    
    describe CreditCard do
      describe '#valid?' do
        context 'expired' do
          it 'is not valid' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).not_to be_valid
          end
        end
      end
    
      describe '#number_is_right_length?' do
        context 'no spaces' do
          context 'right length' do
            it 'returns true' do
              valid = CreditCard.number_is_right_length?('Visa', '3843111122223333')
              expect(valid).to be true
            end
          end
        end
      end
    
      describe '#expired?' do
        context 'expired' do
          it 'returns true' do
            expect(CreditCard.expired?('02/17')).to be true
          end
        end
    
        context 'not expired' do
          it 'returns false' do
            expect(CreditCard.expired?('02/30')).to be false
          end
        end
      end
    end

    Future-Proofing

    Lastly, this most recent test has an unacceptable flaw. I’m checking that the card is not expired if the date is February 2030. As of 2018, February 2030 is in the future, but what happens if this program survives until March 2030? My test suite will break even though the code still works.

    So instead of making the dates hard-coded, let’s make them relative to the current date.

    require 'rspec'
    require './credit_card'
    
    describe CreditCard do
      describe '#valid?' do
        context 'expired' do
          it 'is not valid' do
            cc = CreditCard.new('3843111122223333', '02/17', 'Visa')
            expect(cc).not_to be_valid
          end
        end
      end
    
      describe '#number_is_right_length?' do
        context 'no spaces' do
          context 'right length' do
            it 'returns true' do
              valid = CreditCard.number_is_right_length?('Visa', '3843111122223333')
              expect(valid).to be true
            end
          end
        end
      end
    
      describe '#expired?' do
        let(:this_year) { Date.today.strftime('%y').to_i }
    
        context 'expired' do
          it 'returns true' do
            expect(CreditCard.expired?("02/#{this_year - 1}")).to be true
          end
        end
    
        context 'not expired' do
          it 'returns false' do
            expect(CreditCard.expired?("02/#{this_year + 1}")).to be false
          end
        end
      end
    end

    The Current State of the Code

    Here’s what our class looks like right now:

    require 'date'
     
    class CreditCard
      def initialize(number, expiration_date, brand)
        @number = number
        @expiration_date = expiration_date
        @brand = brand
      end
     
      def valid?
        if self.class.expired?(@expiration_date)
          false
        else
          self.class.number_is_right_length?(@brand, @number)
        end
      end
     
      def self.expired?(expiration_date)
        month, year = expiration_date.split('/').map(&:to_i)
        year += 2000
        DateTime.now.to_date > Date.new(year, month)
      end
     
      def self.number_is_right_length?(brand, number)
        if brand == 'American Express'
          number.gsub(/\s+/, '').length == 15
        else
          number.gsub(/\s+/, '').length == 16
        end
      end
    end

    This code actually doesn’t look super great. It’s arguably not even better than what we started with. Here are some things we could do to improve it:

    • Change the `if/else` in `valid?` to use a guard clause
    • DRY up the length checks in `expired?`
    • Add test coverage for everything that’s not covered yet—which is a lot
    • Put the “magic numbers” of 15 and 16 into constants

    I also am not sure I’m a huge fan of the class methods. What would it look like if we changed those back to instance methods?

    The Fully Refactored Version

    Here’s what I came up with after further refactoring:

    require 'date'
    
    class CreditCard
      NUMBER_LENGTH = 16
      NUMBER_LENGTH_AMEX = 15
    
      def initialize(number, expiration_date, brand)
        @number = number
        @expiration_date = expiration_date
        @brand = brand
      end
    
      def valid?
        number_is_right_length? && !expired?
      end
    
      def expired?
        DateTime.now.to_date > Date.new(*expiration_year_and_month)
      end
    
      def expiration_year_and_month
        month, year = @expiration_date.split('/').map(&:to_i)
        [year + 2000, month]
      end
    
      def number_is_right_length?
        stripped_number.length == correct_card_length
      end
    
      def correct_card_length
        if @brand == 'American Express'
          NUMBER_LENGTH_AMEX
        else
          NUMBER_LENGTH
        end
      end
    
      def stripped_number
        @number.gsub(/\s+/, '')
      end
    end

    In addition to the bullet points above, I made some methods smaller by pulling certain functionality out into new ones.

    Here’s the final test suite:

    require 'rspec'
    require './credit_card'
    
    describe CreditCard do
      let(:this_year) { Date.today.strftime('%y').to_i }
      let(:future_date) { "02/#{this_year - 1}" }
      let(:past_date) { "02/#{this_year + 1}" }
    
      describe '#valid?' do
        context 'expired' do
          it 'is not valid' do
            cc = CreditCard.new('1111111111111111', '02/17', 'Visa')
            expect(cc).not_to be_valid
          end
        end
      end
    
      describe '#number_is_right_length?' do
        context 'no spaces' do
          context 'right length' do
            it 'returns true' do
              cc = CreditCard.new('1111111111111111', future_date, 'Visa')
              expect(cc.number_is_right_length?).to be true
            end
          end
        end
    
        context 'American Express' do
          it 'needs to be 15 numbers long' do
            cc = CreditCard.new('111111111111111', future_date, 'American Express')
            expect(cc.number_is_right_length?).to be true
          end
        end
    
        context 'non-American-Express' do
          it 'needs to be 16 numbers long' do
            cc = CreditCard.new('1111111111111111', future_date, 'Visa')
            expect(cc.number_is_right_length?).to be true
          end
        end
      end
    
      describe '#expired?' do
        context 'expired' do
          it 'returns true' do
            cc = CreditCard.new('1111111111111111', future_date, 'Visa')
            expect(cc).to be_expired
          end
        end
    
        context 'not expired' do
          it 'returns false' do
            cc = CreditCard.new('1111111111111111', past_date, 'Visa')
            expect(cc).not_to be_expired
          end
        end
      end
    end

Ruby Testing Micro-Course

What This Is and Why It Exists

This free “micro-course” is designed to help you get started with testing in Ruby—just Ruby, no Rails.

The idea is that it’s easier to get comfortable with testing Ruby by itself than it would be to try to get started testing Ruby on Rails which involves so many more parts.

Who This Micro-Course Is For

This course is for people who are new to automated testing, but not new to programming altogether. Experience with Ruby will be helpful to you but it’s not necessary.

Start the Course

Enter your name and email below to start the course.

Ruby Testing Micro-Course, Lesson 4

Lesson 1 / Lesson 2 / Lesson 3 / Lesson 4

Review of Lesson 3

In Lesson 3 we added the concept of success or failure to the `check_in_guest` method.

Then I gave you the chance to write a test to ensure that when a guest is checked out the guest’s room gets freed up.

What We’ll Do In Lesson 4

This final lesson will be a short one. I’m going to show you what my test looks like for checking out a guest. Then I’ll give you a suggestion for work you can continue on your own.

Here’s the test I wrote for freeing up a room:

it 'frees up the room' do
  hotel.check_in_guest('Roy Orbison', 302)
  hotel.check_out_guest('Roy Orbison')
  expect(hotel.check_in_guest('George Harrison', 302)).to be true
end

I’m checking Roy Orbison into room 302, then checking him out. Then I’m saying that if I try to check George Harrison into room 302, the check-in should be successful.

If we run the tests, this new test will fail. I’ll show you that in a second. First here’s my test in the context of the full test suite.

describe Hotel do
  let(:hotel) { Hotel.new }

  describe 'checking in a guest' do
    context 'room is available' do
      it 'allows check-in' do
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end

      it "adds the guest to the hotel's guest list" do
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).to include 'George Harrison'
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end

      it "does not add the guest to the hotel's guest list" do
        hotel.check_in_guest('Roy Orbison', 302)
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).not_to include 'George Harrison'
      end
    end
  end

  describe 'checking out a guest' do
    it "removes the guest from the hotel's guest list" do
      hotel.check_in_guest('Buddy Holly', 303)
      hotel.check_out_guest('Buddy Holly')
      expect(hotel.guests).not_to include 'Buddy Holly'
    end

    it 'frees up the room' do
      hotel.check_in_guest('Roy Orbison', 302)
      hotel.check_out_guest('Roy Orbison')
      expect(hotel.check_in_guest('George Harrison', 302)).to be true
    end
  end
end

And like I said above, if we try to run this, the new test will fail. We can’t check a new guest into the room because currently our `check_out_guest` method doesn’t do anything to free up the room.

$ rspec hotelier_spec.rb 
.....F

Failures:

  1) Hotel checking out a guest frees up the room
     Failure/Error: expect(hotel.check_in_guest('George Harrison', 302)).to be true
     
       expected true
            got false
     # ./hotelier_spec.rb:62:in `block (3 levels) in <top (required)>'

Finished in 0.02631 seconds (files took 0.14313 seconds to load)
6 examples, 1 failure

Failed examples:

rspec ./hotelier_spec.rb:59 # Hotel checking out a guest frees up the room

What does it mean to free up a room, exactly? Well, the place we keep our list of occupied rooms is in the (appropriately-named) `@occupied_rooms` instance variable. To free up room 302, we’d need to remove 302 from `@occupied_rooms`.

So we’ll add `@occupied_rooms.delete(room_number)` to `check_out_guest`. The `check_out_guest` method doesn’t know anything about a `room_number` right now, so we’ll also have to modify the method to take a `room_number` argument.

Since we modified the function signature for `check_out_guest`, we’ll also have to change all the instances where it’s used. All these changes are highlighted below.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
    @occupied_rooms = []
  end

  def check_in_guest(guest_name, room_number)
    return false if @occupied_rooms.include?(room_number)
    @guests << guest_name
    @occupied_rooms << room_number
    true
  end

  def check_out_guest(guest_name, room_number)
    @guests.delete(guest_name)
    @occupied_rooms.delete(room_number)
  end
end

describe Hotel do
  let(:hotel) { Hotel.new }

  describe 'checking in a guest' do
    context 'room is available' do
      it 'allows check-in' do
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end

      it "adds the guest to the hotel's guest list" do
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).to include 'George Harrison'
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end

      it "does not add the guest to the hotel's guest list" do
        hotel.check_in_guest('Roy Orbison', 302)
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).not_to include 'George Harrison'
      end
    end
  end

  describe 'checking out a guest' do
    it "removes the guest from the hotel's guest list" do
      hotel.check_in_guest('Buddy Holly', 303)
      hotel.check_out_guest('Buddy Holly', 303)
      expect(hotel.guests).not_to include 'Buddy Holly'
    end

    it 'frees up the room' do
      hotel.check_in_guest('Roy Orbison', 302)
      hotel.check_out_guest('Roy Orbison', 302)
      expect(hotel.check_in_guest('George Harrison', 302)).to be true
    end
  end
end

Now, if we run our test suite, everything passes.

$ rspec hotelier_spec.rb
......

Finished in 0.00723 seconds (files took 0.08605 seconds to load)
6 examples, 0 failures

What Now?

Congratulations on completing this micro-course. If you’d like to keep going, here are some ideas for what you can do next.

Separate the test suite from the application code. In this course I put everything in one file for simplicity and ease. That’s not what you’d do for a production application though. Try to separate the application code and the test suite into two different files.

Keep track of room/guest association. Right now we’re not keeping explicit track of which guests are in which rooms. There’s nothing preventing us from, say, checking Roy Orbison into room 302 and then checking him out of room 563. It also feels unnatural that we have to specify a room number when checking out a guest. One would expect the application to keep track of that. See if you can make this happen.

Add payments. What if checking a guest out required the guest to pay? You could also add some simple reporting capabilities. How much revenue has the hotel earned?

Add dates. It would make sense to keep track of the dates when guests check in and check out. If combined with payments and reporting, this would give you the ability to show the hotel’s revenue for a certain time period.

Conclusion

Thanks for taking the time to learn about Ruby testing. If you have any questions, I usually suggest using Stack Overflow, but you’re also welcome to leave a question in the comments.

If you made it this far, I’d encourage you to use the link below to get this full micro-course via email so you have it forever.

Ruby Testing Micro-Course, Lesson 3

Lesson 1 / Lesson 2 / Lesson 3 / Lesson 4

Review of Lesson 2

In Lesson 2 I gave you a chance to compare the test you wrote in Lesson 1 to my version of the same test, and I showed you exactly how I came up with that test. Then we began planning features for checking guests into specific rooms.

Also in Lesson 2, we established that the following things should be true for room functionality:

  1. Add the guest to the hotel’s guest list (we’re already testing for this)
  2. Disallow another guest from checking into that same room
  3. Decrease the total number of available rooms

What We’ll Do In Lesson 3

In Lesson 3 we’re going to write a test that ensures we don’t allow a guest to check into a room that’s already checked out by another guest.

Then I’ll give you a chance to write your own test that says, “When we check a guest out of a room, that room should get freed up.”

Test for Room Availability

Right now our `check_in_guest` method takes whatever `guest_name` we give it and happily adds that `guest_name` onto the `@guests` array. There’s no concept of success or failure. The `check_in_guest` method just always works, regardless of whether it really should work or not.

Let’s change this. Let’s make it so if we try to check a new guest into a room that’s already checked out by another guest, `check_in_guest` will return `false`.

And of course, `check_in_guest` should return `true` if the room is free and the new guest was successfully checked in.

We’ll begin by adding two test cases: one for the “room is available” case and one for the “room is not available” case.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name, room_number)
    @guests << guest_name
  end

  def check_out_guest(guest_name)
    @guests.delete(guest_name)
  end
end

describe Hotel do
  describe 'checking in a guest' do
    it "adds the guest to the hotel's guest list" do
      hotel = Hotel.new
      hotel.check_in_guest('George Harrison', 302)
      expect(hotel.guests).to include 'George Harrison'
    end

    context 'room is available' do
      it 'allows check-in' do
        hotel = Hotel.new
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end
    end
  end

  it 'can check a guest out' do
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly', 303)
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

When we run our test suite, both of our new tests will fail. We’re expecting `true` and `false` but `check_in_guest` doesn’t return `true` or `false`. It returns the value of `@guests`.

rspec hotelier_spec.rb                                             
..FF                                           

Failures:                                      

  1) Hotel checking in a guest room is available allows check-in                              
     Failure/Error: expect(hotel.check_in_guest('George Harrison', 302)).to be true           
                                               
       expected true                           
            got #<Array:70174165542720> => ["George Harrison"]                                
     # ./hotelier_spec.rb:30:in `block (4 levels) in <top (required)>'                        

  2) Hotel checking in a guest room is not available disallows check-in                       
     Failure/Error: expect(hotel.check_in_guest('George Harrison', 302)).to be false          
                                               
       expected false                          
            got #<Array:70174163166020> => ["Roy Orbison", "George Harrison"]                 
     # ./hotelier_spec.rb:38:in `block (4 levels) in <top (required)>'                        

Finished in 0.02482 seconds (files took 0.13683 seconds to load)                              
4 examples, 2 failures                         

Failed examples:                               

rspec ./hotelier_spec.rb:28 # Hotel checking in a guest room is available allows check-in     
rspec ./hotelier_spec.rb:35 # Hotel checking in a guest room is not available disallows check-in   

Let’s make `check_in_guest` return something closer to what our tests our expecting.

This might seem like a silly move but we can make one of the two tests pass by simply hard-coding a return value of `true` for `check_in_guest`.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name, room_number)
    @guests << guest_name
    true
  end

  def check_out_guest(guest_name)
    @guests.delete(guest_name)
  end
end

describe Hotel do
  describe 'checking in a guest' do
    it "adds the guest to the hotel's guest list" do
      hotel = Hotel.new
      hotel.check_in_guest('George Harrison', 302)
      expect(hotel.guests).to include 'George Harrison'
    end

    context 'room is available' do
      it 'allows check-in' do
        hotel = Hotel.new
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end
    end
  end

  it 'can check a guest out' do
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly', 303)
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Now we only have one test failure instead of two.

rspec hotelier_spec.rb                                             
...F                                           

Failures:                                      

  1) Hotel checking in a guest room is not available disallows check-in                       
     Failure/Error: expect(hotel.check_in_guest('George Harrison', 302)).to be false          
                                               
       expected false                          
            got true                           
     # ./hotelier_spec.rb:39:in `block (4 levels) in <top (required)>'                        

Finished in 0.0173 seconds (files took 0.0875 seconds to load)                                
4 examples, 1 failure                          

Failed examples:                               

rspec ./hotelier_spec.rb:36 # Hotel checking in a guest room is not available disallows check-in

Again, this might seem like a silly thing to have done but I like to do stuff like this when I’m developing tests. It gives me one less thing to think about when I’m working on the next step. There’s also just something nice and neat about “expected false and got true” versus “expected false and got some crazy unexpected value”.

Returning False If Room Is Occupied

Now we have to do the “harder” work of making `check_in_guest` actually return false if the room is occupied.

Right now a `Hotel` instance doesn’t have a concept of occupied rooms at all. All it knows is which guests are checked into the hotel.

Let’s add a list of occupied rooms to our class. When we check in a guest, `check_in_guest` will now not only add `guest_name` to our list of guests but it will add `room_number` to a list of occupied rooms.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
    @occupied_rooms = []
  end

  def check_in_guest(guest_name, room_number)
    @guests << guest_name
    return false if @occupied_rooms.include?(room_number)
    @occupied_rooms << room_number
    true
  end

  def check_out_guest(guest_name)
    @guests.delete(guest_name)
  end
end

Now all our tests will pass.

$ rspec hotelier_spec.rb 
....

Finished in 0.00483 seconds (files took 0.08766 seconds to load)
4 examples, 0 failures

Here’s the full `hotelier_spec.rb` file in case you got lost in the course of following the above steps.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
    @occupied_rooms = []
  end

  def check_in_guest(guest_name, room_number)
    @guests << guest_name
    return false if @occupied_rooms.include?(room_number)
    @occupied_rooms << room_number
    true
  end

  def check_out_guest(guest_name)
    @guests.delete(guest_name)
  end
end

describe Hotel do
  describe 'checking in a guest' do
    it "adds the guest to the hotel's guest list" do
      hotel = Hotel.new
      hotel.check_in_guest('George Harrison', 302)
      expect(hotel.guests).to include 'George Harrison'
    end

    context 'room is available' do
      it 'allows check-in' do
        hotel = Hotel.new
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end
    end
  end

  it 'can check a guest out' do
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly', 303)
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Bug: Guest Will Get Added Even If Room Is Occupied

You might have noticed an issue with our latest version of `check_in_guest`.

def check_in_guest(guest_name, room_number)
  @guests << guest_name
  return false if @occupied_rooms.include?(room_number)
  @occupied_rooms << room_number
  true
end

Notice how it adds `guest_name` to `@guests` before it checks to see whether the room is actually available or not.

I added this mistake intentionally to show that our current test suite is not as complete as it needs to be.

Let’s fix this bug, but first let’s add a test that ensures that attempting to check in a guest to an unavailable room does not add that guest to the hotel’s guest list.

describe Hotel do
  describe 'checking in a guest' do
    it "adds the guest to the hotel's guest list" do
      hotel = Hotel.new
      hotel.check_in_guest('George Harrison', 302)
      expect(hotel.guests).to include 'George Harrison'
    end

    context 'room is available' do
      it 'allows check-in' do
        hotel = Hotel.new
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end

      it "does not add the guest to the hotel's guest list" do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).not_to include 'George Harrison'
      end
    end
  end

  it 'can check a guest out' do
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly', 303)
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Our test will of course fail because we haven’t fixed `check_in_guest` yet.

rspec hotelier_spec.rb:45
Run options: include {:locations=>{"./hotelier_spec.rb"=>[45]}}
F

Failures:

  1) Hotel checking in a guest room is not available does not add the guest to the hotel's guest list
     Failure/Error: expect(hotel.guests).not_to include 'George Harrison'
       expected ["Roy Orbison", "George Harrison"] not to include "George Harrison"
     # ./hotelier_spec.rb:49:in `block (4 levels) in <top (required)>'

Finished in 0.01358 seconds (files took 0.09659 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./hotelier_spec.rb:45 # Hotel checking in a guest room is not available does not add the guest to the hotel's guest list

Now let’s fix `check_in_guest` by moving the “is this room occupied?” check to be on the first line.

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
    @occupied_rooms = []
  end

  def check_in_guest(guest_name, room_number)
    return false if @occupied_rooms.include?(room_number)
    @guests << guest_name
    @occupied_rooms << room_number
    true
  end

  def check_out_guest(guest_name)
    @guests.delete(guest_name)
  end
end

Now all five of our tests pass.

rspec hotelier_spec.rb                                             
.....                                          

Finished in 0.00603 seconds (files took 0.09014 seconds to load)                              
5 examples, 0 failures       
describe Hotel do
  describe 'checking in a guest' do
    context 'room is available' do
      it 'allows check-in' do
        hotel = Hotel.new
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end

      it "adds the guest to the hotel's guest list" do
        hotel = Hotel.new
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).to include 'George Harrison'
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end

      it "does not add the guest to the hotel's guest list" do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).not_to include 'George Harrison'
      end
    end
  end

  it 'can check a guest out' do
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly', 303)
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Here’s the full `hotelier_spec.rb` file for reference.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
    @occupied_rooms = []
  end

  def check_in_guest(guest_name, room_number)
    return false if @occupied_rooms.include?(room_number)
    @guests << guest_name
    @occupied_rooms << room_number
    true
  end

  def check_out_guest(guest_name)
    @guests.delete(guest_name)
  end
end

describe Hotel do
  describe 'checking in a guest' do
    it "adds the guest to the hotel's guest list" do
      hotel = Hotel.new
      hotel.check_in_guest('George Harrison', 302)
      expect(hotel.guests).to include 'George Harrison'
    end

    context 'room is available' do
      it 'allows check-in' do
        hotel = Hotel.new
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end

      it "does not add the guest to the hotel's guest list" do
        hotel = Hotel.new
        hotel.check_in_guest('Roy Orbison', 302)
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).not_to include 'George Harrison'
      end
    end
  end

  it 'can check a guest out' do
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly', 303)
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Refactoring

Our test suite is a little repetitive. We have `hotel = Hotel.new` all over the place. Let’s make our tests a little more DRY.

describe Hotel do
  let(:hotel) { Hotel.new }

  describe 'checking in a guest' do
    context 'room is available' do
      it 'allows check-in' do
        expect(hotel.check_in_guest('George Harrison', 302)).to be true
      end

      it "adds the guest to the hotel's guest list" do
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).to include 'George Harrison'
      end
    end

    context 'room is not available' do
      it 'disallows check-in' do
        hotel.check_in_guest('Roy Orbison', 302)
        expect(hotel.check_in_guest('George Harrison', 302)).to be false
      end

      it "does not add the guest to the hotel's guest list" do
        hotel.check_in_guest('Roy Orbison', 302)
        hotel.check_in_guest('George Harrison', 302)
        expect(hotel.guests).not_to include 'George Harrison'
      end
    end
  end

  it 'can check a guest out' do
    hotel.check_in_guest('Buddy Holly', 303)
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Exercise: Freeing Up a Room

If checking a guest into a room makes that room unavailable, checking a guest out of a room should make that room available again. Right now our code doesn’t do that.

See if you can write a test that ensures that checking out a guest frees up that room. Feel free to also write the code that makes that test pass, but not before you write the test.

I’ll see you in the final lesson, Lesson 4, where you can see how I wrote my test for freeing up a room.

Continue to Lesson 4 >>>

Ruby Testing Micro-Course, Lesson 1

Lesson 1 / Lesson 2 / Lesson 3 / Lesson 4

What We’re Going to Do in This Micro-Course

This micro-course is just Ruby and RSpec. No Rails, no database, no HTML.

We’re going to build a Ruby application designed for the purpose of managing a hotel. We’ll build this application over the course of four easy-to-follow lessons.

Lesson 1 Overview

First I’m going to show you a super tiny version of the application we’re going to build. We’re going to examine this tiny “codebase” together and make sure we understand exactly how it works.

The app includes one test that I’ve written. You’ll run the test on your machine. You’ll modify the app so the test breaks, then you’ll fix the app again.

Finally, you’ll add a tiny feature to the app as well as a test for it. Then you can compare your test to the test I wrote so you can see how you did.

Here we go!

The “Codebase”

Here’s the program we’ll be starting with. Don’t worry if you don’t understand 100% of it right now. We’ll be examining this program in detail to make sure there are no mysteries by the time we’re done.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name)
    @guests << guest_name
  end
end

describe Hotel do
  it 'can check in a guest' do
    hotel = Hotel.new
    hotel.check_in_guest('George Harrison')
    expect(hotel.guests).to include 'George Harrison'
  end
end

Let’s take a closer look at this code, starting with the test.

First Test: Checking In a Guest

Here’s the single test that I’ve included in this program:

hotel = Hotel.new
hotel.check_in_guest('George Harrison')
expect(hotel.guests).to include 'George Harrison'

You can probably intuit what this test is doing even if you’re a newcomer to Ruby and/or testing. If we were to translate this test to English, we might say, “When I check George Harrison into a hotel, I expect that hotel’s guests to include George Harrison.”

Let’s actually run the test.

Running the Test

If you don’t already have RSpec installed, do so now by running gem install rspec.

We can run our test by running the rspec command and specifying the name of our test file.

$ rspec hotelier_spec.rb 
.

Finished in 0.00542 seconds (files took 0.12365 seconds to load)
1 example, 0 failures

The test should pass. But there’s a problem.

How do we know that our test is really testing something? Our test could be passing because our code works, or our test could be passing because we made a mistake in the test.

The only way we can be sure is to intentionally make the test fail, then make it pass again.

Making the Test Fail

Let’s modify our code in some way such that it will no longer make the test pass. If we comment out the body of `check_in_guest`, that should do it.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name)
    #@guests << guest_name
  end
end

describe Hotel do
  it 'can check in a guest' do
    hotel = Hotel.new
    hotel.check_in_guest('George Harrison')
    expect(hotel.guests).to include 'George Harrison'
  end
end

Let’s run our test file again to verify that the test fails now.

$ rspec hotelier_spec.rb 
F

Failures:

  1) Hotel can check in a guest
     Failure/Error: expect(hotel.guests).to include 'George Harrison'
       expected [] to include "George Harrison"
     # ./hotelier_spec.rb:19:in `block (2 levels) in <top (required)>'

Finished in 0.01585 seconds (files took 0.0873 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./hotelier_spec.rb:16 # Hotel can check in a guest

The test does in fact fail. We can now be sure that our test is testing what we think it’s testing.

Before you move on, uncomment the body of check_in_guest and run the test file again to make sure that we’re back in a working state.

Exercise: Write Your Own Test

Now that you’ve seen me write a test, I want you to write your own test.

Here’s your exercise: write a test that will check a guest out. Let’s verify that when we check someone in and then check him or her out, the hotel no longer lists that person among its guests.

Here’s the code you can use as a starting point. I added an `it` block for you to put your test inside of.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name)
    @guests << guest_name
  end
end

describe Hotel do
  it 'can check a guest in' do
    hotel = Hotel.new
    hotel.check_in_guest('George Harrison')
    expect(hotel.guests).to include 'George Harrison'
  end

  it 'can check a guest out' do
    # Put your own test here
  end
end

When you’re finished, move onto Lesson 2. In Lesson 2 I’ll show you my version of the test. You can compare your work to mine and see how similar or different it is.

By the way, if you’re drawing a total blank, that’s okay. Just move onto the next lesson to see how I wrote the test. You’ll have more opportunities to try writing tests later in the micro-course.

Continue to Lesson 2 >>>