Week 3 - Test Driven Development

Week 3 - Test Driven Development

·

16 min read

#docking_station_spec.rb
require 'docking_station'

describe DockingStation do
    let(:station) { DockingStation.new }

    describe '#release_bike' do
      it { is_expected.to respond_to :release_bike}

      it 'releases working bikes' do
        bike = Bike.new
        station.dock(bike)
        bike = station.release_bike
        expect(bike).to be_working
      end

      it 'raises an error when empty' do
        bike = Bike.new
        expect { station.release_bike }.to raise_error("Docking Station is empty")
      end

      it 'should not release bikes if there are no bikes' do
        expect { subject.release_bike }.to raise_error("Docking Station is empty")
      end
    end

    describe '#dock' do
      it { is_expected.to respond_to(:dock).with(1).argument }

      it 'docks something' do
            bike = Bike.new
            subject.dock(bike)
            expect(subject.bikes).to include bike
      end

      it 'raises an error if when full' do
        station.capacity.times { station.dock Bike.new }
        expect { station.dock Bike.new }.to raise_error("Docking Station is full")
      end
    end


    it { is_expected.to respond_to(:bikes) }


    describe 'initialisation' do
      it 'should set the capacity when passed a number at initialisation' do
      station = DockingStation.new(30)
      30.times { station.dock Bike.new}
      expect{ station.dock Bike.new }.to raise_error("Docking Station is full")
      end

    end
#docking_station.rb
require_relative 'bike'

class DockingStation
  DEFAULT_CAPACITY = 20

  attr_reader :bikes
  attr_accessor :capacity

  def initialize(capacity = DEFAULT_CAPACITY)
    @bikes = []
    @capacity = capacity 
  end

  def release_bike
    raise "Docking Station is empty" if empty?
    @bikes.pop
  end

  def dock(bike)
    raise "Docking Station is full" if full?
    @bikes << bike
  end

  private

  def full? 
    return @bikes.length >= capacity
  end

  def empty?
    @bikes.empty?
  end
end
#bike.rb
class Bike
  def working?
    true
  end
end

RSpec

Using let helper method

Use let to define values to use in tests. The value will be cached across multiple calls in the same example but not across examples. So what that means is that if you call let multiple times in the same block, it evaluates the let block just once (the first time it’s called). When you call it again in the second it block, the let helper method will get re-evaluated again.

describe DockingStation do
    let(:station) { DockingStation.new }

    describe '#release_bike' do
      it 'releases working bikes' do
        bike = Bike.new
        station.dock(bike)
        bike = station.release_bike
        expect(bike).to be_working
      end
end

Predicate Matchers

For methods that return true or false, there is a convention in Ruby to end the method name with a ? RSpec provides a shorthand syntax, so the test expectation can be expressed way more concisely.

RSpec supports a one-liner syntax for setting an expectation on the subject.

it { is_expected.to respond_to :release_bike} is similarly defined as expect(subject).to to respond_to...

describe DockingStation do
  it 'responds to release_bike' do
    expect(subject).to respond_to :release_bike
  end
end

# TO

describe DockingStation do
    let(:station) { DockingStation.new }

    describe '#release_bike' do
      it { is_expected.to respond_to :release_bike}
    end
end

Another example

  describe '#metropolis?' do
    context 'without population' do
      it 'is false' do
        expect(subject.metropolis?).to be false
      end
    end

# TO 

 describe '#metropolis?' do
    context 'without population' do
      it { is_expected.to_not be_metropolis }
    end
 end

Before and After hooks

They provide a way to define and run the setup. The before(:each) method is where we define the setup code. When you pass the :each argument, you are instructing the before method to run before each example in your Example Group i.e. the it block inside the describe #land in the code below.

In the line airport.land(plane) we are calling the land method on the airport class and passing in the plane. This way the code is cleaner in our tests and we do not need to have to add it each it block.

describe Airport do
  let(:airport) { Airport.new }
  let(:airport) { Plane.new }

  describe '#land' do
    it { is_expected.to respond_to(:land) }

    before(:each) do
      airport.land(plane)
    end

    it 'lands a plane and puts it in the hangar' do
      expect(airport.hangar).to include plane
    end
end

before(:all) - runs before all examples
after(:each) - runs after each example
after(:all) - runs after all examples

expect {} and expect ()

expect{} will tell expect to observe the whole process while expect() will evaluate inside the () first and check if the return value matches the matchers following.

If we want to just check the return value then we can simply use the expect(). The argument is evaluated immediately. Otherwise if we want to check for any changes we can use the expect{}, with this we can pass a code block inside. The block contents isn't executed immediately, it's execution is determined by the method you're calling.

The expect statement below will actually equal this:

expect do
    25.times {airport.land(plane}
end.to raise_error('Airport is full')
  it 'prevents landing if the airport is full' do
      expect { 25.times { airport.land(plane) } }.to raise_error('Airport is full')    
    end

RSpec syntax

  • respond_to

You're asserting that the Airport class when instantiated that the land method returns true, which means it is present inside the Airport class.

describe Airport do
  let(:airport) { Airport.new }
  it { is_expected.to respond_to(:land) }
end
  • be_

We can use the 'be' matcher to match whether the specified object is true/false/nil

expect(obj).to be_truthy  # passes if obj is truthy (not nil or false)
expect(obj).to be_falsey  # passes if obj is falsy (nil or false)
expect(obj).to be_nil     # passes if obj is nil
expect(obj).to be         # passes if obj is truthy (not nil or false)
  • raise_error

Use the raise_error matcher to specify that a block of code raises an error. You can also use the raise_exception which will both work the same.

 it 'should not release bikes if there are no bikes' do
        expect { subject.release_bike }.to raise_error("Docking Station is empty")
      end
  • with()

    Use with to specify the expected arguments. A message expectation constrained by with will only be satisfied when called with matching arguments.
with(1, true)
=begin
will match with foo(1, true)
=end
  • include

Can use the include matcher to specify that a collection includes one or more expected objects. It succeeds if any object of the given collection passes the specified matcher. This works on any object that responds to #include? (such as a string or array):

Below we expect the subject.bikes (which is stored as an array) to include the bike that was docked.

   it 'docks something' do
            bike = Bike.new
            subject.dock(bike)
            expect(subject.bikes).to include bike
   end

Ruby

Initialising the object and Setting Instance Variables

Set an attribute on an instantiated Object using an @ instance variable

Whenever you call the method new on a class, as in Person.new, the class will create a new instance of itself. It will then, internally, call the method initialize on the new object. Doing so it will pass all the arguments that you passed to new on to the method initialize.

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

Person.new("Ada")
The string "Ada" will be passed on to our initialize method, and end up being assigned to the local variable name.

The body of the initialize method assigns the value of the local variable name to an instance variable @name. The instance variable of @name will be accessible in any instance method in a particular instance of a class, they are visible everywhere in the object, that is, in every method that the object has.

attr_reader and attr_accessor

Use attr_reader to read the data on an object.

We cannot get the data, the name from the person object because it’s private. So to make it publicly available, we need a method. Methods in Ruby are public by default.

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

 def name
    @name
  end
end

attr_reader does exactly the same thing, only now you can shorten that code like so.

class Person
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

attr_accessor lets you read and change the data on an object and in the background provides a getter and a setter method.

class Person
  attr_accessor :name, :age, :sex, :email

  def initialize(name)
    @name = name
  end
end

john = Person.new("John")
john.name = "Jim"
puts john.name # => Jim

Initializing a default value and using a constant variable

The DockingStation class has an initialize method with two instance variables, bikes and capacity. It has parameters which outlines the default values set to 20. When we create an instance of the DockingStation class, the initialize method is automatically called.We can see that the capacity is set to a constant variable which is outlined in uppercase as DEFAULT_CAPACITY = 20.

class DockingStation
  DEFAULT_CAPACITY = 20

  attr_reader :bikes
  attr_accessor :capacity

  def initialize(capacity = DEFAULT_CAPACITY)
    @bikes = []
    @capacity = capacity 
  end

Ruby Constants

We can use :: as a Namespace operator: we use it to access the DEFAULT_CAPACITY constant defined within the DockingStation class.

it 'should not let the bike dock if storing more than 20 bikes' do
      expect { DockingStation::DEFAULT_CAPACITY.times do
        subject.dock Bike.new
      end }.to raise_error("Docking Station is full")
    end
class DockingStation

    DEFAULT_CAPACITY = 20

  attr_reader :bikes

  def initialize

    @bikes = []
  end

  def dock(bike)
    if full?
      raise "Docking Station is full"
    else
      @bikes << bike
    end
  end

  private
  def full? 
    return @bikes.length >= DEFAULT_CAPACITY
  end
end

require and require_relative

require is a method that is used when you want to reference and execute code that is not written in your current file. The method takes in a path in the form of a string as an argument and there are two ways the string can be formatted — either as an absolute path or a shortened name.

# absolute path
require './app/example_file.rb'
# shortened name
require 'example_file'

The shortened strings always depend on the listed directories in $LOAD_PATH. $: is a global variable that is used for looking up external files. You may see this used in your bin/environment.rb like this:

bin/environment.rb
$LOAD_PATH << './app'

require is usually called when requiring gems loaded in gemfile. Though require can be used to both execute gems and external dependencies, the preferable method to load relative paths is require_relative, it's a subset of require and is a convenient method to use when you are referring to a file that is relative to the current file you are working on (basically, within the same project directory).

Private methods

Classes sometimes want to keep certain methods private: methods that aren’t supposed to be called from outside of the object. Only the object itself is supposed to use them internally, from other methods. By default all methods are public!

Private methods call ONLY be called from inside the class where it’s defined. We can declare private methods by simply typing private and any methods below private will all be private methods.

def dock(bike)
    raise "Docking Station is full" if full?
    @bikes << bike
  end

  private

  def full? 
    return @bikes.length >= capacity
  end

A user will not need to check if the docking station is full. A user just needs to know if they can dock the bike. So internally we can create a private method to check if the docking station is full before docking their bike.

fail/raise expectations

Signal exceptions using the fail method.
Use raise only when catching an exception and re-raising it (because you'tr purposefully raising an exception).

begin
  fail 'Oops'
rescue => error
  raise if error.message != 'Oops'
end
# Explicit
raise RuntimeError.new("You messed up!")

# ...produces the same result
raise RuntimeError, "You messed up!"

# ...produces the same result. But you can only raise 
# RuntimeErrors this way
raise "You messed up!"

Guard clauses

Guard clauses are an awesome way to make a piece of code more succinct and understandable.

def my_method(variable)
  if variable == 'great'
    # do something great
  else
    return nil
  end
end

# TO

def my_method(variable)
  return nil unless variable == 'great'
  # do something great
end