Author Archives: Jason Swett

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

Why small stories are better than big ones

A small story is easier to understand than a big story.

It’s easier to have a crisp definition of done for a small story than for a big one.

It’s easier to review a pull request for a small story than for a big story.

It’s less risky to deploy a small story than a big one.

Small stories couple with frequent deployments make it easier to maintain dev/prod parity.

Cycle time is smaller for small stories than big ones. This means less need to do sub-branches off of feature branches.

“But not everything can be small”

Maybe that’s true. Not every story can be small but it can at least be as small as possible.

And in my experience most development teams stop whittling down stories well before they reach “as small as possible”.

Here’s an example of a time I saw a big piece of work made into small parts. I once worked on a set of stories that involved renaming a concept from “sellable” and “unsellable” to “for sale” and “not for sale”. One of the first stories our team implemented and deployed was to simply rename the database table’s column and some of the referencing code.

The story didn’t involve changing the UI. We didn’t even change our API endpoints to reflect the new name. Most teams would probably consider this level of granularity to be silly, but we found it prudent to spread the work across as many small stories (and deployments) as possible to minimize the risk that any one deployment would cause a problem. Usually, a way can be found to make things small.

How to get started with Rails testing

Where to start with Rails testing

Getting started with Rails testing can be overwhelming because there’s so much to learn. I think the learning process can be made easier by following the following three steps.

Get familiar with testing principles

In order to write tests in Rails it’s necessary to have some understanding of the tools that are used for Rails testing. But also, perhaps more than that, you need to have an understanding of the technology-agnostic principles that apply no matter what language or framework you’re using.

The principles of automated testing are quite numerous. Just as you could spend a lifetime studying programming and never run out of things to learn, you could probably spend a lifetime studying testing and never run out of things to learn.

So the trick early on is to find the few testing principles that you need in order to get started and ignore all the rest. You can learn other principles as you go.

Here are a few principles I think are helpful to be familiar with at the beginning.

Test-driven development

I personally think TDD is life-changingly great. At the same time, I wouldn’t recommend being concerned with following TDD to the letter when you’re first getting started with testing.

The fundamental principle of TDD is the red-green-refactor loop. First you write a failing test for the behavior you want to bring into existence. Then you write the code to make it pass, only concerning yourself with making the test pass and not with code quality at all. Then, once the test is passing, go back to the code and refactor it to make it nice.

When I was getting started with testing I had many occasions where I wanted to write the test first but I wasn’t sure how. What should you do if that happens to you? My advice would be to give yourself permission to break the TDD “rules” and just write the code before the test. As you gain more experience with testing it will get easier and easier to write the test first.

Four-phase testing

Automated tests tend to consist of four steps: setup, exercise, verification and teardown. Here’s an example in MiniTest.

class UserTest < Minitest::Test
  def test_soft_delete_user
    user = User.create!(email: 'test@example.com') # setup
    user.update_attributes!(active: false)         # exercise
    assert user.active == false                    # assertion
    user.destroy!                                  # teardown
  end
end

In the first step, setup, we create the data that the test needs in order to do its stuff.

In the second step, exercise, we walk the system under test* through the steps that are necessary to get the system into the state we’re interested in.

In the third step, assertion, we ask, “Did the code do the thing it was expected to do?”

Finally, in the teardown, we put the system back the way we found it. (In Rails testing an explicit teardown step is usually not necessary because tests are often run inside of database transactions. The data doesn’t have to get deleted because it never gets persisted in the first place.)

*System under test (SUT) is a fancy way of saying “the part of the application we’re testing”.

Test independence/deterministic tests

It’s important that when a test runs, we get the same result every time. The passing or failing of the test shouldn’t depend on things like the date when the test was run or whether a certain other test was run before it or not.

That’s why the teardown step above is important. If a test leaves behind data after it runs, that data has the potential to interfere with another test that runs after it. Again, in Rails, developers tend not to explicitly destroy each test’s data but rather make use of database transactions to avoid persisting each test’s data in the first place. The transaction starts at the beginning of each test and at the end of each test the transaction is aborted.

Fixtures and factories

The first step of a test, setup, can get quite tedious and repetitive. There are easier ways of bringing the test data into existence than by instantiating objects and creating database records at the beginning of every single test.

There are two ways that setting up test data is normally handled: factories and fixtures. Both strategies have pros and cons. Most projects I’ve worked on use either one or the other but there’s no reason they couldn’t be used together.

Fixtures in Rails are usually defined in terms of YML files. The fixture data is persisted once at the beginning of the test suite run. Each test still runs in a database transaction so that any data modifications the test makes will be undone before the next test is run.

Fixtures have the advantage of speed, although the trade-off is that the data setup is a little bit distant from the test code itself. It might not always be clear what the data is that a test depends on or where it’s coming from.

With factories, the data for each test is defined inside the test code itself instead of in a separate YML file. Factories have the advantage of clarity. If the tests were written well then it’s very clear what data the test depends on to run. The trade-off is speed.

Mocks and stubs

I bring up mocks and stubs to tell you that you can safely ignore them when you’re getting started.

You can get quite a ways with Rails testing before a lack of mock and stub knowledge will hinder you. But if you’re curious, the books xUnit Patterns and Growing Object-Oriented Software, Guided by Tests do a pretty good job of explaining the concepts.

Get familiar with Rails testing tooling

Testing frameworks

The two dominant testing frameworks for Rails are RSpec and MiniTest. RSpec seems to be much more popular although that certainly doesn’t mean it’s better.

“Should I use RSpec or MiniTest?” is a question I see lot. Luckily you can’t really choose wrong. The underlying testing principles are pretty much the same whether you use RSpec or MiniTest. The main difference between the two is that RSpec is built on a domain-specific language (DSL) while MiniTest is just Ruby. A lot of people find that RSpec’s DSL improves readability although the tradeoff is that you have to learn the DSL.

I personally started out with Test::Unit (very similar to MiniTest) when I was doing Rails projects on my own. Later I switched to RSpec because almost every job or freelance client I’ve had used RSpec. So even if you pick the “wrong” testing framework to start with, you can always switch to the other one later.

Since I’m an RSpec guy I’ll focus mainly on RSpec-related tools.

Factory Bot

Factory Bot is the de-facto standard library for Rails. The main benefits of Factory Bot in my view are that it helps DRY up test setup code and that it eliminates the need to come up with arbitrary test data (e.g. fake names, addresses, etc.).

Faker

Factory Bot only goes so far with its ability to generate fake data. If I want to generate things like fake but valid email addresses, phone numbers, etc., I use Faker in conjunction with Factory Bot.

Capybara

Capybara is a tool that lets us use Ruby to drive the browser. This enables us to write tests that exercise the whole application stack including HTML, CSS and JavaScript.

Build some practice projects

Just like with programming itself, the way to get good at testing is to just start doing it.

Here’s the way I generally go about developing a new feature or resource in Rails. (I do a Git commit after each step if not more often.)

  1. Run rails generate scaffold <scaffold name> <options>
  2. Write some validation specs for the model I just created
  3. Write a feature spec for the “happy path” (i.e. all valid inputs) of the “create” and “update” functionality for the resource I just created

For the first few models I create in a Rails project I might not do much more than that. It’s usually not until a few models in that my application builds behavior that’s “interesting” enough to write a meaningful test for.

If you want to learn more about how I write Rails tests, I run free online Rails testing workshops about once a week. All the videos from previous sessions are up on YouTube (linked here). I also have a free Ruby testing micro-course.

RSpec Hello World

class Squarer
  def square(starting_number)
    starting_number * starting_number
  end
end

describe Squarer do
  it 'square a number' do
    squarer = Squarer.new
    expect(squarer.square(2)).to eq(4)
  end
end

You can run this test by naming the file `squarer_spec.rb` and running `rspec squarer_spec.rb` on the command line.

If you don’t already have RSpec installed you can install it by running `gem install rspec`.

Getting Started with Angular CLI

One of the main things I liked about Rails when I first started using it was its command line interface. You get so much leverage by being able to scaffold a whole resource with just one command.

I was very happy when I learned about Angular CLI, which is currently in beta. I’ve been using Yeoman for Angular 1 but as far as I’ve been able to tell so far the Yeoman generators aren’t quite caught up with Angular 2. It’s nice to have a tool for Angular 2 that helps me understand how to structure my project, how to get tests set up, and all that nuts-and-bolts stuff that I don’t want to have to think about when I’m trying to quickly get familiar with a new technology.

In this tutorial I show you how to spin up a sample Angular 2 project on a Rails backend. Since the focus is on Angular and not Rails, I’ve provided a Rails repo for you so you don’t have to think about the Rails part too much.

Setting up the project

You can use my repo as a starting point. We’ll blow away and then rebuild the Angular portion but keep the Rails portion.

$ git clone git@github.com:jasonswett/dream_cars.git
$ cd dream_cars

CD into the project directory, create a new branch for this tutorial, then blow away the Angular code (which I always keep in the “client” directory as my own personal convention).

$ cd dream_cars
$ git checkout -b tutorial
$ rm -rf client
$ git commit -a -m"Delete contents of client directory."

Install the angular-cli package globally.

$ npm install -g angular-cli

Spin up a new Angular 2 project using ng new.

$ ng new dream-cars

Since for my Angular/Rails projects I always keep my Angular code in client, let’s rename dream-cars to client.

$ mv dream-cars client
$ cd client
$ rm -rf .git

We can run ng serve to start a server on port 4200.

$ ng serve

If you navigate to http://localhost:4200, you should see a message that says, “dream-cars works!”

Adding the Car component

Generate the car component.

$ ng generate route car

Modify your src/app/dream-cars.component.html to look like this. What we’re doing here, of course, is adding some navigation links.

<h1>{{title}}</h1>

<nav>
  <ul>
    <li><a [routerLink]="['/']">Home</a></li>
    <li><a [routerLink]="['/car']">Cars</a></li>
  </ul>
</nav>

<router-outlet></router-outlet>

If you go to your browser and click on the “Cars” link, you should see “car works!”.

Pulling in some data from the server

I already have migrations and seed data set up for you in the Rails project. All you have to do is run rake db:setup and start the rails server.

$ rake db:setup
$ rails server

If you navigate to http://localhost:3000/api/cars.json, you should see some data.

Modify src/app/+car/car.component.ts to look like this.

import { Component, OnInit } from '@angular/core';
import { Http, HTTP_PROVIDERS } from '@angular/http';

@Component({
  moduleId: module.id,
  selector: 'app-car',
  templateUrl: 'car.component.html',
  styleUrls: ['car.component.css']
})
export class CarComponent implements OnInit {
  cars: any;

  constructor(public http: Http) {}

  ngOnInit() {
    this.http.get('/api/cars.json')
      .subscribe(response => this.cars = response.json());
  }

}

Notice the cars: any part. That’s probably not the way that should be done. Frankly, I don’t know the right way to do that yet. I’m learning too.

In order for Http to work, you’ll have to add it to your application’s bootstrap in src/main.ts.

import { bootstrap } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { DreamCarsAppComponent, environment } from './app/';
import { Http, HTTP_PROVIDERS } from '@angular/http';

if (environment.production) {
  enableProdMode();
}

bootstrap(DreamCarsAppComponent, [HTTP_PROVIDERS]);

If you now navigate to http://localhost:4200/car, you’ll get an error. This is because our HTTP request is trying to go to http://localhost:4200/api/cars.json, which doesn’t exist. We need http://localhost:3000/api/cars.json.

To make our Angular server on port 4200 talk to our Rails server on port 3000, we can use a proxy. Kill your Angular server process and run this instead:

$ ng serve --proxy http://localhost:3000

This tells the server to route all XHR requests to port 3000. If you refresh the browser, you should no longer see the error.

There’s another problem, though. If you look at the log output for both the Angular server and the Rails server, you’ll see some wacky errors flying across the screen. This has to do with source map files not being included in /dist. You can fix the problem by modifying angular-cli-build.js in the following way.

/* global require, module */

var Angular2App = require('angular-cli/lib/broccoli/angular2-app');

module.exports = function(defaults) {
  return new Angular2App(defaults, {
    vendorNpmFiles: [
      'systemjs/dist/system-polyfills.js',
      'systemjs/dist/system.src.js',
      'zone.js/dist/*.{js,js.map}',
      'es6-shim/es6-shim.js',
      'reflect-metadata/*.{js,js.map}',
      'rxjs/**/*.{js,js.map}',
      '@angular/**/*.{js,js.map}'
    ]
  });
};

With that out of the way, let’s edit our src/app/+car/car.component.html to allow our car data to be shown.

<ul>
  <li *ngFor="let car of cars">
    {{ car.year }}
    {{ car.make }}
    {{ car.model }}
  </li>
</ul>

<car-form></car-form>

If you navigate to http://localhost:4200/car, you should now see something like the following, with a list of cars.

dream cars

There’s a lot I’ve skipped in this tutorial. There are probably some things I’ve done improperly. This is meant to be a quick and dirty intro to Angular CLI so you can get a real full-stack application functioning so you can start tinkering and making progress with Angular 2.