Ruby Testing Micro-Course, Lesson 3

by Jason Swett,

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

Leave a Reply

Your email address will not be published. Required fields are marked *