Category Archives: Programming

All code eventually gets tested, it’s just a question of when and how and by whom

All code eventually gets tested. If a bug is found, it can be found in one of the following ways, in descending order of cost and embarrassment.

A Customer Trying to Use the Feature in Production

Obviously the worst place to find a bug is in production, and the worst person to find it is an end user trying to do his or her job.

This is a bad way to find a bug not just because it blocks your user from using your feature successfully and thus hurts your reputation, but because you might not even immediately find out that the bug exists. Not every user who finds a bug immediately reports that bug to the vendor’s development team. When I find a bug in Gmail, for example, I don’t make an attempt to report the bug to the Gmail team, I just suffer through it.

Even if the customer reports the bug, the bug probably won’t get directly reported to you, the developer. It will probably get reported to a support person, then get communicated to your manager, then communicated to you. By this point the bug may gain high visibility inside your organization, providing embarrassment to you and your team.

Discovering bugs in production is of course inevitable but all possible measures should be taken to avoid it.

Your Boss/Client Trying to Use It

The next worst way to find a bug is for your boss or client to find it.

I don’t mean when your boss/client serves in a QA tester role. I mean when your boss/client tries to use the feature with the expectation that it’s already finished and verified by you to be working.

The reason that this is bad is obvious: it erodes the trust your boss or client has in your competence as a developer. It especially hurts to get back a comment like, “Hey, let me know when this is 100% working.”

Not only is there the embarrassment and trust erosion but a concrete cost as well. The back-and-forth between you and your boss/client lengthens the development cycle for the feature, and therefore increases the cost to develop that feature.

Manually Testing it Yourself

Catching a bug by manually testing the feature yourself is way better than letting your boss, client or end user find it.

If you discover a silly bug, you might still feel a little embarrassed, but it will be a small, private embarrassment. You also keep the development cycle shorter by skipping the back-and-forth of “Hey boss, this is done,” “Hey developer, it’s not actually done.”

An Automated Test

The least expensive way to catch a bug is to write an automated test that makes the bug physically impossible.

Some developers perceive that writing automated tests takes “extra” time. That’s an inaccurate way to look at it. All testing takes time. The difference is that with a manual test, the time is paid each time you carry out the test, and with automated testing the time is paid all at once upfront. It’s like the difference between paying cash for a $250,000 house versus getting a mortgage and paying $500,000 for the same house over the course of 30 years. It potentially only takes a small handful of runs of a manual test before you’ve used up more time than it would have taken to write an automated test.

“So You’re Saying Automated Tests Are The Answer?”

My point is not that automated tests are a better alternative to all the other forms of testing. My point is that I believe automated tests are, in most cases, the least expensive testing method and the best first line of defense.

Imagine a secret government research facility that’s protected by an armed guard, locking doors and safe containing sensitive documents. If a Russian spy is after the documents, it’s obviously better for the spy to get caught by the guard than for the spy to make it past the guard and locked doors and make it all the way to the safe. But that doesn’t mean it’s a smart idea to do away with the doors and safe and only have the guard. Defense should exist at all levels practicable.

Same thing with automated testing. Just because it’s cheaper for a bug to get caught by an automated test doesn’t mean it’s a good idea to do away with manual testing. Protection should exist at all levels. It’s just a good idea to try to catch as many bugs at the earliest levels as possible.

How to Get RSpec to Skip View Specs When You Generate Scaffolds

I personally don’t find much value in tests for views, helpers, routes or requests in most cases.

It’s annoying to have to delete these files each time you generate a new scaffold. Fortunately, it’s possible to configure RSpec not to generate these files.

Below is an example of how you can exclude these types of spec (and more) from being generated when you generate a new scaffold.

require_relative 'boot'

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
require "sprockets/railtie"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module MyApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    # Don't generate system test files.
    config.generators.system_tests = nil

    config.generators do |g|
      g.test_framework :rspec,
        fixtures:         false,
        view_specs:       false,
        helper_specs:     false,
        routing_specs:    false,
        request_specs:    false,
        controller_specs: false
    end
  end
end

Ruby/Rails testing glossary

Ruby-Specific Terms

Capybara
Integration testing tool. Capybara’s GitHub page describes it as an “acceptance test framework for web applications” but everyone I know uses the term integration test when they talk about Capybara.
Cucumber
A tool that lets you write test scenarios in English commands and then tie those English commands to code. For example, a Cucumber file could contain the line `Given I am on the login page`. In a different file, the text `Given I am on the login page` would map to some Ruby code that navigates to the login page.

I personally advise people not to use Cucumber.

Database Cleaner
The tests in a test suite should be runnable in any order. If a test leaves behind extra data or depends on data set up in a previous test, there’s a good chance the ability to run the tests in any order has been lost. Database Cleaner is a tool that helps ensure every test case starts and ends with a blank database.
factory_bot
“A library for setting up Ruby objects as test data”, according to factory_bot’s GitHub page. I think that’s a pretty good description. If I were to say it in my own words, I’d describe factory_bot as a library that helps define factories for the purpose of conveniently creating test data.
Faker
Faker is, for my purposes, a tool that generates random values so I don’t have to manually come up with arbitrary phone numbers, names of people, etc. when testing. Faker is also handy when I need test values to be unique, e.g. when having two people with the same name would be a problem. I use Faker in conjunction with factory_bot.
MiniTest
A Ruby testing framework.
RSpec
A Ruby testing framework. In my perception, RSpec is by far the most popular testing framework in the Rails community.
System Tests
A feature added to Rails core in Rails 5.1 to make integration testing easier. Uses Capybara. If you’d like to learn more about System Tests, Noel Rappin wrote a pretty good Rails System Tests guide.
Test::Unit
A Ruby testing framework.
timecop
A library that makes it easy to test time-dependent code. Offers features like “time travel” and “time freezing”.

Non-Ruby-Specific Terms

Acceptance Test
A test that checks whether a feature meets its business requirements. A certain feature may work exactly as the developer and designer intended, but if the developer and designer designed and built the wrong thing, the acceptance test would fail.
Assertion
A condition that evaluates to true if the feature behaves properly and false if the feature contains a bug. The analogous term in RSpec is “expectation”.
CI Server
A server that aids in the practice of continuous integration. A CI server typically runs the test suite anytime a code change is pushed and notifies the development team in the case of a test suite failure.
CircleCI
A CI server product.
Continuous Integration
The practice of continuously merging together the work of all the developers on a development team. The benefit of continuous integration is that it helps teams avoid the painful process of integrating large chunks of work, which is often tedious and error-prone.
Dependency Injection
A technique where an object’s dependencies are passed as arguments, making it easier to test objects in isolation. See this post of mine for an example.

Factory
There’s a design pattern called the Factory Pattern or the Factory Method Pattern and it exists independently of anything to do with tests. I basically think of a factory as code that spits out an object of some sort.

I included the Factory term here because factories are very relevant to how I write tests. Rather than manually spinning up a bunch of arbitrary data using fixtures, I prefer to define factories that can hide away irrelevant details, allowing me to focus only on the parts I care about. For example, if I want to create a restaurant for testing purposes, I might not want to have to specify the restaurant’s phone, address, hours, or any of those other details, even though they may be required attributes. Most of the time I just want to specify the restaurant’s name and nothing else. Factories can make this job very easy and convenient.

Fixture
A way of setting up test data. In Rails, fixtures are defined using YAML files. I prefer factories over fixtures for a number of reasons. One of the main reasons is that I find fixtures to be too “distant” from the tests, making it somewhat mysterious what data is available for any particular test.
Flapping test
A test that sometimes fails and sometimes passes, seemingly at random. Also known as a “flickering” test. They’re often a symptom of one test case “leaking” into subsequent test cases and affecting their behavior.
Edge Case
A path through a UI that isn’t exercised frequently or that is unlikely to be exercised.
End-to-End Test
A test that exercises all the layers of an application stack. It could be said that an end-to-end test is more inclusive than an integration test and that an integration test only has to combine two or more layers of an application stack to be considered an integration test. In practice I’ve found that people use the terms “integration test” and “end-to-end test” fairly interchangeably and if you want total clarity, you have to ask what exactly the person means.
Environment Parity
Environment parity is a broader version of the dev/prod parity term from the Twelve-Factor App principles. The idea is that every difference between two environments is an opportunity for a bug to be present in one environment that is not present in the other. For this reason it’s helpful to keep the test, development and production environments as similar to each other as possible.
Happy Path
A normal/default/valid path through a feature. (I like to think of the opposite as the “sad path”.)
Integration Test
A test that combines two or more layers of an application stack, or that combines two or more systems. See also: end-to-end test.
Mock
A “fake” object. A mock is useful when you want to test a certain object, but don’t want to deal with the hassle of bringing that object’s dependencies into the picture. The dependencies can instead be mocked.
Regression Testing
A type of testing meant to ensure that previously-working functionality has not been broken by newly-developed functionality.
Selenium
A tool for automating browsers. Also, a chemical element.
Stub
A term that may or may not mean the exact same thing as mock. Mock and stub are not terms that seem to have crisp industry consensus.
System Under Test
The system that’s being tested.
Test Case
I don’t think I can improve on Wikipedia‘s definition: “A specification of the inputs, execution conditions, testing procedure, and expected results that define a single test to be executed to achieve a particular software testing objective”.
Test Coverage
The percentage of an application covered by automated tests.
Test Environment
An independent instance of an application used specifically for the purpose of testing.
Test Suite
A collection of tests. An application might have one or many (or zero!) test suites.
Test-Driven Development
The practice of developing features by first writing tests that specify the behavior of the desired functionality, then writing the code to make those tests pass.
Unit Test
A fine-grained test that tests a very specific piece of functionality in isolation. Note: what Rails developers often refer to “unit tests” are not technically unit tests since those tests involve the database.

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

Testing Anti-Pattern: Setup Data Leak

In general it’s better to have tests than to not have tests. Most people agree on this.

But just like poor-quality code, poor-quality tests get hard to maintain over time.

One anti-pattern I see in testing, an anti-pattern that makes tests harder and harder to maintain as the tests grow, is what I’ll call “setup data leak”. Here’s what it looks like.

Let’s imagine we have a `User` class. Maybe the user class can do things related to accounts, roles and posts.

In order to test accounts, roles and posts, we have to create some account, role and post data.

A Bad Test

describe User do
  let(:user) { create(:user) }
  let(:account) { create(:account, user: user) }
  let(:role) { create(:role, user: user) }
  let(:post) { create(:post, user: user) }

  describe 'account-related stuff' do
    # tests go here
  end

  describe 'role-related stuff' do
    # tests go here
  end

  describe 'post-related stuff' do
    # tests go here
  end
end

Why is this test bad? Because it defines all the test data in one place. Why is that bad? Two reasons.

The “Single Glob of Data” Phenomenon

Let’s imagine that this test file grows over time and instead of setting up 4 things at the top, we set up 15. How could anyone be sure which of those 15 things is needed for any particular test? Maybe some tests only need 3 of those 15 things. Maybe other tests need all 15. Maybe still others need 8 of them.

Since we don’t know what’s needed for any particular test, we basically have to assume that everything is needed for every test until we prove otherwise on a case-by-case basis. So our 15 things aren’t 15 things anymore but one single horrifying glob of data.

Mystery Guests

Let’s again imagine that this test file grows over time. Deep within the bowels of one particular test you see `role`. What’s `role`? You look to the immediate vicinity and find nothing. Eventually you see that `role` is defined at the very top of the file even though it’s only used in one place.

A mystery guest is a value that appears in a test case but is defined far away from the test case.

If a value is truly used everywhere then there’s no reason not to define it at the very top of the test. But if it’s only used in one place, there’s no reason not to define it right next to the place it’s used.

A Better Test

describe User do
  let(:user) { create(:user) }

  describe 'account-related stuff' do
    let(:account) { create(:account, user: user) }
    # tests go here
  end

  describe 'role-related stuff' do
    let(:role) { create(:role, user: user) }
    # tests go here
  end

  describe 'post-related stuff' do
    let(:post) { create(:post, user: user) }
    # tests go here
  end
end

This test is better than the first one because there’s no “setup data leak” issue. All the setup data is defined close to where it’s needed. To put it another way, all the setup data is defined in the narrowest scope possible.

Did you find this article useful? See all my Ruby/Rails testing articles.

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 2

Lesson 1 / Lesson 2 / Lesson 3 / Lesson 4

Review of Lesson 1

At the end of Lesson 1 you were asked to write a test for checking a guest out.

If you haven’t completed Lesson 1, go back and complete it before you proceed, or at least try. Don’t feel bad if you tried and failed to come up with a test in Lesson 1. I’m about to show you my test and how I came up with it. You’ll have more chances soon to make another attempt.

What We’ll Do In Lesson 2

In Lesson 2 I’m going to let you compare your test to mine. Then you’ll have a chance to see how I came up with the test I wrote.

Then we’ll start to add a new feature to our hotel application, room numbers.

Compare Your Test to Mine

Here’s the test (and new application code) I wrote for checking out a guest. The new code is highlighted.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name)
    @guests << guest_name
  end

  def check_out_guest(guest_name)
    @guests.delete(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
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly')
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Now let me show you the steps I took to come up with this test.

How I Got Here

Before I wrote any code I asked myself, “How would I verify that a check-out feature is working?”

My answer was that I would check a guest in, then check that same guest back out. The hotel should no longer include that guest in its guest list.

Then I translated this sequence of actions—check a guest in, check a guest out, check for that guest—into code. Below is what I added.

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
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly')
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Then I ran the test, knowing it wouldn’t work. Here’s what I got.

$ rspec hotelier_spec.rb
.F

Failures:

  1) Hotel can check a guest out
     Failure/Error: hotel.check_out_guest('Buddy Holly')
     
     NoMethodError:
       undefined method `check_out_guest' for #<Hotel:0x007f8b3e021d60 @guests=["Buddy Holly"]>
       Did you mean?  check_in_guest
     # ./hotelier_spec.rb:25:in `block (2 levels) in <top (required)>'

Finished in 0.00649 seconds (files took 0.13307 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./hotelier_spec.rb:22 # Hotel can check a guest out

The test of course didn’t work. The test didn’t even fail, it errored. My next step was to simply get the test to stop erroring.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name)
    @guests << guest_name
  end

  def check_out_guest(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
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly')
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Here’s what this test run looks like:

$ rspec hotelier_spec.rb
.F

Failures:

  1) Hotel can check a guest out
     Failure/Error: expect(hotel.guests).not_to include 'Buddy Holly'
       expected ["Buddy Holly"] not to include "Buddy Holly"
     # ./hotelier_spec.rb:29:in `block (2 levels) in <top (required)>'

Finished in 0.02465 seconds (files took 0.0844 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./hotelier_spec.rb:25 # Hotel can check a guest out

The test no longer errors out. It just fails.

If we were to translate this test failure to English it might be something like “I expected the list of guests not to include Buddy Holly, but it did include Buddy Holly”.

Now all that’s left to do is make the test pass.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name)
    @guests << guest_name
  end

  def check_out_guest(guest_name)
    @guests.delete(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
    hotel = Hotel.new
    hotel.check_in_guest('Buddy Holly')
    hotel.check_out_guest('Buddy Holly')
    expect(hotel.guests).not_to include 'Buddy Holly'
  end
end

Now the test will pass.

$ rspec hotelier_spec.rb
..

Finished in 0.00467 seconds (files took 0.0835 seconds to load)
2 examples, 0 failures

What’s Next

So far you’ve gotten a chance to try writing your own test. Then you watched me write a test for the same feature.

Next I’m going to let you watch me develop a new feature and write tests at the same time. Then I’ll give you a chance to try writing another test of your own.

Room Numbers

In a real hotel you don’t just check into the hotel generally. You of course check into a specific room.

Let’s add some room functionality to our application.

I’m going to begin adding room functionality using coding by wishful thinking. In other words, I’ll ask myself, “What code do I wish I could use to check in a guest?” Below is what I came up with.

require 'rspec'

class Hotel
  attr_accessor :guests

  def initialize
    @guests = []
  end

  def check_in_guest(guest_name)
    @guests << guest_name
  end

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

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

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

The `check_in_guest` method currently doesn’t take a second argument, so one of my tests will fail.

$ rspec hotelier_spec.rb
F.

Failures:

  1) Hotel can check a guest in
     Failure/Error:
       def check_in_guest(guest_name)
         @guests << guest_name
       end
     
     ArgumentError:
       wrong number of arguments (given 2, expected 1)
     # ./hotelier_spec.rb:10:in `check_in_guest'
     # ./hotelier_spec.rb:22:in `block (2 levels) in <top (required)>'

Finished in 0.00386 seconds (files took 0.08159 seconds to load)
2 examples, 1 failure

Failed examples:

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

The way to make this test pass of course is to make the `check_in_guest` method take another argument. I’ll also have to change my other test’s use of `check_in_guest` to specify a room number.

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
  it 'can check a guest in' do
    hotel = Hotel.new
    hotel.check_in_guest('George Harrison', 302)
    expect(hotel.guests).to include 'George Harrison'
  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

This new specifying-a-room-number thing technically works in the sense that the tests pass, but it’s a little unsatisfying. As of right now, specifying a room doesn’t actually do anything.

It seems like checking a guest into a room should do at least three things:

  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

We’ll address these things in Lesson 3.

Continue to Lesson 3 >>>

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